Checkers: a game, a thinking computer and a music box, all in Objeck

A complete checkers (English draughts) game in a few thousand lines of Objeck, built to show three things at once: game programming (a 60 frames-a-second SDL2 window, input, animation), classic AI (alpha-beta search, Monte Carlo tree search through the System.AI library) and learning (the computer tunes its own evaluation by playing itself, and its music learns habits from example phrases). Everything is drawn and played from code: no pictures, no sound files, nothing to license.

cd programs/examples/checkers
obc -src checkers_engine.obs,checkers_game.obs,checkers_ai.obs,checkers_logic.obs,checkers_draw.obs,checkers_chars.obs,checkers_fx.obs,checkers_music.obs,checkers_audio.obs,checkers.obs -lib sdl2,sdl_game,ai,json,collect -dest checkers.obe
obr checkers.obe

Run it from this folder so it finds checkers_weights.json (without it levels 9 and 10 use the weights built into the source, which are the same numbers).

Playing

Click a piece, then click where it goes (or use the arrow keys and Enter). Jumping is compulsory, as in the real game: when you can jump, only the pieces that can are highlighted. Other keys: H hint, U take a move back, N new game, F flip the board, S sound on/off, E save the music so far as a .mid file, Esc menu, Q quit. The menu picks the piece set, the level (1 to 10), your colour (or Watch the computer play itself), flip, fun mode, animations, reduced motion, sound, volume and the music style. Your choices are remembered in .checkers.json in your home folder.

Rules

English / American checkers on an 8 x 8 board: men move one square diagonally forward and capture by jumping forward over an enemy piece; a jump is mandatory, and a jump may continue with the same piece (a multi-jump is one move). A man that reaches the far row is crowned a king (a king moves and jumps in all four diagonal directions) and its move ends there, even if a further jump would be possible. You lose when you have no legal move. The game is drawn by threefold repetition, or after 40 moves each (80 plies) without a capture or a crowning.

Piece sets

Ten sets, all drawn with SDL2 shapes, each with its own board colours; the first neo set is what you get on a first run. Four are plain discs: Classic (flat with a rim), Glossy (gradient with a highlight), Engraved (grooved concentric rings) and Modern (flat teal against amber), each with a clear crown for kings. Four are crazy fun characters, every piece a little creature, and two are tech-neo gadgets (checkers_chars.obs):

SetSide oneSide two
Critterscats: cream, pointy ears, whiskersdogs: brown, floppy ears, snout
Monstersone-eyed purple monsters with hornssquare steel robots with an antenna
Snackspizzas: round, pepperoniburgers: stacked bun, patty, lettuce
Spacesuns: yellow with raysmoons: pale, craters, a sleepy cap
Neo Duckrubber-duck debuggers: yellow, a beak that opens, a ladybird on the headcoffee mugs: dark navy, neon cyan rim, steam, a handle
Neo Glitchglitchy pixel ghosts: pale lavender, magenta rim, an RGB-split echorebooting robots: charcoal, lime rim, a visor and a turning reload arrow

The two sides of a pair differ in outline (round and pointy against square, round against stacked, rays against craters) and strongly in lightness, so they stay apart for people who cannot tell colours apart, and at the smallest board size. The creatures blink, bob and sway their ears and antennae, and their faces react to the game: worried when they could be captured, cheering after a capture, dizzy (with stars) while being captured, winking at a gift, shaken in a disaster. A king is the same creature in a cape and a crown. With Animations: off the faces are still. The recipes are a table of numbers (CkChars->Recipe).

The neo sets add a look to match: the board is dark glass over a circuit board (traces and vias on the playing squares), with a neon frame whose glow is a few translucent strokes under a bright one, a hairline grid, faint scanlines and a slow wave of light that sweeps the rows about once every four seconds (never faster than 1 Hz, and still when Animations are off or Reduced motion is on). Every piece has a soft neon bloom in its outline colour. A king is the same gadget with a neon halo and a blinking antenna (twice a second). A captured gadget shows the blue screen of death (a sad smiley and lines of error text). The panels and buttons are dark glass with a neon edge that glows when pointed at. The two sides differ in lightness (yellow against navy, lavender against charcoal) and in outline colour and shape, so they stay apart without colour vision. The opponent's face has loading-spinner eyes while it thinks, and in the neo sets half its one-liners come from tech support ("Have you tried turning it off and on again?", "Error 404: your piece was not found.").

The ten levels

LevelNameHow it plays
1Dozy Daveany legal move (but always takes a compulsory jump)
2Clumsy Clarathe best move one ply ahead; 40% of the time a random one
3Wobbly Walt2 plies ahead; 22% random moves
4Careful Kitthe library's Minimax, 3 plies
5Sly SamMinimax, 4 plies
6Professor PawnMinimax, 6 plies
7Captain Crownour alpha-beta (move ordering, jump extension), 6 plies
8Madame Minimaxalpha-beta, 8 plies
9Deep Thinkeriterative deepening with the learned evaluation, up to 10 plies / 2 million positions
10Grandmaster Gizmothe same, up to 13 plies / 6 million positions (several seconds a move)

The computer thinks on its own thread, so the window never freezes: the pieces keep wobbling and the face keeps sweating while a deep search runs.

Does a higher level really play better? The tournament

checkers_tournament.obs plays level N against level N-1 for 10 random openings, each played twice with the colours swapped (20 games per pair, fun mode off). The openings are 4 random plies from a seeded generator, so the table below is exactly reproducible:

obc -src checkers_engine.obs,checkers_game.obs,checkers_ai.obs,checkers_tournament.obs -lib ai,json -dest checkers_tournament.obe
obr checkers_tournament.obe 10 2 10 checkers_weights.json 1
pairN winsdrawsN lossesN scoreas Redas White
L2 vs L1190195.0%10.0/109.0/10
L3 vs L2140670.0%8.0/106.0/10
L4 vs L3171287.5%9.0/108.5/10
L5 vs L4135277.5%7.5/108.0/10
L6 vs L5812070.0%7.5/106.5/10
L7 vs L6812070.0%8.0/106.0/10
L8 vs L7145182.5%8.0/108.5/10
L9 vs L8810265.0%5.0/108.0/10
L10 vs L91010075.0%8.0/107.0/10

("N score" counts a win as 1 and a draw as half. The strong levels draw a lot: checkers is a drawish game.) Levels 4 to 6 use the library Minimax class over CkState, a System.AI.GameState wrapper of the position; the same wrapper lets the library's MonteCarloTreeSearch play, and the tests check that it takes a free piece.

How it learns

The evaluation is a weighted sum of eight facts about a position, counted for Red minus White: men, kings, how far the men have advanced, men still on the home row, pieces on the eight central squares, men on the edge files, the number of simple moves available, and kings in the middle. A positive total is good for Red. Which weights are good is not obvious, so checkers_train.obs finds them by playing the game:

  1. Hill climbing against the champion. The weights start as "just count the pieces" (men 100, kings 100, everything else 0). Each round makes a challenger by adding random noise to the champion's weights and plays it against the champion for 60 games (30 openings, both colours, 3 plies deep). A challenger that scores more than 56% becomes the new champion. It is evolution in miniature: a few hundred rounds take minutes.
  2. TD(lambda) self-play is available too (obr checkers_train.obe 7 4000 ...): the program plays itself and, after every move, nudges the weights so that a position predicts the positions that follow it (the idea behind TD-Gammon). On this problem it learned a defensive style that scored 17 to 29% against the hand-tuned weights, so the shipped weights come from the hill climb.

Everything is seeded, so the same command always writes the same file:

obc -src checkers_engine.obs,checkers_game.obs,checkers_ai.obs,checkers_train.obs -lib ai,json -dest checkers_train.obe
obr checkers_train.obe 7 0 300 checkers_weights.json 100

Result of the shipped run (seed 7, 300 rounds, about 8 minutes on a laptop-class core): men 100, kings 137, advancement -4.7, back row -1.6, centre 0.9, edge 8.2, mobility 2.3, king centre 17.2. Over fresh openings, at depth 4:

Match (120 to 200 games)Score of the first
piece-counting weights (where training starts) vs the hand-tuned weights30%
trained weights vs the hand-tuned weights51% (200 games)
trained weights vs piece counting with kings worth 1.5 men67%

So starting from nothing but "count the pieces", the program learned by itself an evaluation as good as the one I tuned by hand (and clearly better than material alone). The hand-tuned weights are CkEval->Hand, used by levels 1 to 8; levels 9 and 10 use the learned ones. Honest summary: the learned weights match, they do not beat, the hand-tuned ones; the strength of the top levels comes mostly from search depth.

How the music learns

The music is generated while you play, from formulas, as MIDI-style note events (track, note, velocity, time) that two back ends can play: a built-in software synthesizer, or a Standard MIDI File (format 1, drums on channel 10) that Save song writes to your home folder.

Measuring the music offline

The music can be rendered with no window and no sound card, so it can be measured. checkers_wav.obs renders eight bars of every style with the deep-bass setting off and on and prints, for each, the peak sample, the share of samples at the limit (clipping), the RMS (average loudness) and how much of the sound is below 120 Hz (the RMS left after two one-pole low-pass filters, a 12 dB per octave slope, divided by the RMS of everything):

obc -src checkers_engine.obs,checkers_music.obs,checkers_wav.obs -dest checkers_wav.obe
obr checkers_wav.obe                    # the table below
obr checkers_wav.obe 2 deep reggae.wav  # style 2 (Reggae), deep bass, written as a WAV
styledeep basssecondspeakclippedRMSbass below 120 Hz
Hip-hopoff21.81.000.00%0.19271.2%
Hip-hopon21.81.000.08%0.29187.3%
Modern beatsoff14.41.000.00%0.20371.8%
Modern beatson14.41.000.05%0.30586.3%
Reggaeoff26.01.000.00%0.16068.8%
Reggaeon26.01.000.00%0.24587.4%
Technooff15.41.000.00%0.20173.1%
Technoon15.41.000.04%0.24284.3%
Classicaloff21.80.640.00%0.15169.0%
Classicalon21.80.970.00%0.24488.7%

Deep bass makes every style louder in the bass (the RMS rises by a half or more and the bass share rises by 12 to 20 points) without clipping more than a tenth of a percent of the samples; classical has no drums to push its peak above 0.64 without it. The regression test makes the same measurements on three bars of every style and fails if deep bass does not add low-frequency energy, if more than half a percent of the samples clip, or if techno has less bass than classical. The measuring stick is itself tested with a 60 Hz and a 2000 Hz tone.

Fun mode (off by default)

Early in a game, when you have lost more pieces than the computer, a seeded dice roll after a computer capture (one chance in three) can bring either a gift (the computer "gives back" a man on a free square in your home rows) or a natural disaster (lightning, an earthquake, or a gust of wind removes one computer piece). Events are announced before they happen, appear in the move list marked with *, never remove your pieces, happen at most twice a game and only in your first 12 moves, are deterministic for the game's seed (so a replay gets the same events), and Undo takes them back. The engine tests cover all of that. The tournament and the training never use fun mode.

"Early on" can mean early in the game or at the early levels, so there is a menu setting with both readings: levels 1-3 (the default when fun mode is switched on) and all levels. Both only act in the opening window above.

The playful layer

Pieces squash and stretch as they hop, wobble when picked up and look at the pointer; captures go poof with a comic word; a chain of jumps counts up (Double! Triple! Ka-boom! ...) with a bigger shake and more sparkles each time, and the last jump is shown in slow motion (a chain that makes a king throws confetti); a crown pops on with a hop and sparkles; a win gets confetti and a bouncing trophy. When nothing has happened for six seconds one piece does something silly (waves, yawns, sneezes and nudges its neighbours, peeks). Each of the ten opponents has a face that follows the pointer, wears its own hat, sweats while thinking and has a line for every mood; click it and it answers, press B for a party, and level 10 introduces itself as a boss. The words and lines are lists in CkFxTable, so adding one is adding a string. Animations: off shows every move instantly with no effects; Reduced motion keeps the move animation but turns off squash, particles, shaking and flashing; Screen shake can be switched off on its own. All three are saved in the settings file. Nothing blinks or pulses faster than about twice a second, and a lightning flash lasts a quarter of a second once. The effects cost about 2.5 ms of the 16.7 ms frame with everything on (--demo measures it on your machine).

Checking it without a human

The tests

programs/regression/checkers_engine.obs runs about 440 checks in a couple of seconds with no window and no sound. It is generated from the files in this folder by make_regression_test.py (the regression runners compile one file per test); CI fails when it is stale. It covers:

The files

FileWhat it is
checkers_engine.obsbitboards, legal moves, make-move, perft (no graphics)
checkers_game.obsa whole game: history, undo, win and draw rules, fun-mode events
checkers_ai.obsevaluation, alpha-beta search, GameState wrapper, the ten levels
checkers_logic.obsclicks and keys as intents, settings, level names, the thinking thread
checkers_draw.obsboard, plain piece sets, crowns, buttons, text
checkers_chars.obsthe creature and gadget piece sets (critters, monsters, snacks, space, neo duck, neo glitch)
checkers_fx.obsparticles, pop-up words, the ten faces, the lists of words
checkers_music.obsstyle table, Markov chains, note events, MIDI writer, synthesizer, effects, CkMeasure
checkers_wav.obsrenders and measures the music offline, writes WAV files
checkers_audio.obsplaying it: the sound-card queue, MIDI fallback, song export
checkers.obsthe window and the main loop
checkers_train.obs, checkers_tournament.obsthe learner and the level tournament
checkers_tests.obs, make_regression_test.pythe tests, and the generator of the regression file
checkers_weights.jsonthe trained weights

How the board is stored

A bitboard is one 64-bit number in which bit row*8+column says whether that square holds a piece; checkers only uses the 32 dark squares. Red, White and "is a king" are three numbers. A step to the upper right is a shift by 7 bits; whether a piece can jump is a lookup in a small table of "the square two steps away"; a move is applied with a few ANDs and XORs and taken back by restoring five numbers, so the search never allocates. That is why a search visits about 750,000 positions a second even in this interpreted-and-JIT language.