summaryrefslogtreecommitdiff
path: root/docs/chapter31/examples
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-14 16:23:39 -0800
commit207febef9924c8fa5391185e75d42a7ea480bb38 (patch)
tree68ec6225a89fa1178c32fdf659b9b4f2c3327288 /docs/chapter31/examples
parent259f60b105eb770bc6f7e3bd36d91f2b217e9315 (diff)
downloadSimple-207febef9924c8fa5391185e75d42a7ea480bb38.tar.gz
Simple-207febef9924c8fa5391185e75d42a7ea480bb38.zip
Chapter 31: Software Drawing, Time, and Graphical Gameslinear
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/chapter31/examples')
-rw-r--r--docs/chapter31/examples/A_helloWorld.smp2
-rw-r--r--docs/chapter31/examples/Breakout.smp364
-rw-r--r--docs/chapter31/examples/BubbleSort.smp38
-rw-r--r--docs/chapter31/examples/Capitalize.smp23
-rw-r--r--docs/chapter31/examples/Clock.smp128
-rw-r--r--docs/chapter31/examples/Dijkstra.smp71
-rw-r--r--docs/chapter31/examples/FileIO.smp52
-rw-r--r--docs/chapter31/examples/Graphics.smp17
-rw-r--r--docs/chapter31/examples/README.md108
-rw-r--r--docs/chapter31/examples/TicTacToe.smp132
-rw-r--r--docs/chapter31/examples/TicTacToeGraphical.smp168
11 files changed, 1103 insertions, 0 deletions
diff --git a/docs/chapter31/examples/A_helloWorld.smp b/docs/chapter31/examples/A_helloWorld.smp
new file mode 100644
index 00000000..4ed6abdf
--- /dev/null
+++ b/docs/chapter31/examples/A_helloWorld.smp
@@ -0,0 +1,2 @@
+sys.io.p("Hello, World!\n");
+return 0;
diff --git a/docs/chapter31/examples/Breakout.smp b/docs/chapter31/examples/Breakout.smp
new file mode 100644
index 00000000..b1d0b443
--- /dev/null
+++ b/docs/chapter31/examples/Breakout.smp
@@ -0,0 +1,364 @@
+// Clear 24 x 8 bricks. Arrows or mouse move; Space/click serves; P pauses;
+// R restarts; Q/Escape exits. Any argument runs ten frames as a smoke test.
+u32 WIDTH = 1008, HEIGHT = 760, COLS = 24, ROWS = 8;
+int LEFT = 24, RIGHT = 984, TOP = 84, BOTTOM = 718;
+int BRICK_TOP = 152, BRICK_DX = 40, BRICK_DY = 22, BRICK_W = 36, BRICK_H = 18;
+int RADIUS = 5, PADDLE_HALF = 56, PADDLE_Y = 674;
+int READY = 0, PLAY = 1, WON = 2, LOST = 3;
+int KEY_LEFT = 263, KEY_RIGHT = 262; // MiniFB key codes; gfx.LEFT is a mouse button.
+i64 STEP_ms = 8, FRAME_ms = 16;
+u32 BACKGROUND = 0x0b1220, WHITE = 0xeef7ff, MUTED = 0x8aabc4, CYAN = 0x4ce0ef;
+val COLORS = new u32[~]{0xff5268,0xff8050,0xffb548,0xffdb58,
+ 0x9bdd58,0x45d8b0,0x49b9f5,0x8d94ff};
+val DIGITS = new u8[~][~]{"0","1","2","3","4","5","6","7","8","9"};
+
+struct Game {
+ u8[] bricks = new u8[COLS*ROWS]{int i -> 1;};
+ int score = 0, remaining = COLS*ROWS, lives = 3, depth = 1, phase = READY;
+ bool boosted = false, backWall = false, paused = false;
+ f64 x = 504.0, y = 663.0, dx = 0.4, dy = -0.916515138991168;
+
+ // Fixed 8 ms simulation, split further to travel at most two pixels per step.
+ // Even at maximum boosted speed a ball cannot tunnel through an 18 px brick.
+ val _tick = { int direction ->
+ if( paused || phase == WON || phase == LOST ) return 0;
+ _paddle(paddle+direction*6.4);
+ if( phase == READY ) { x = paddle; y = PADDLE_Y-RADIUS-6; return 0; }
+ f64 seconds = 0.008; // 8ms
+ while( seconds > 0.000000001 && phase == PLAY ) {
+ f64 dt = 2.0/speed;
+ if( dt > seconds ) dt = seconds;
+ seconds -= dt;
+ f64 oldX = x, oldY = y;
+ x += dx*speed*dt;
+ y += dy*speed*dt;
+ if( x < LEFT+RADIUS && dx < 0.0 ) {
+ x = 2.0*(LEFT+RADIUS)-x; dx = -dx; _cue(1);
+ }
+ if( x > RIGHT-RADIUS && dx > 0.0 ) {
+ x = 2.0*(RIGHT-RADIUS)-x; dx = -dx; _cue(1);
+ }
+ if( y < TOP+RADIUS && dy < 0.0 ) {
+ y = 2.0*(TOP+RADIUS)-y; dy = -dy;
+ backWall = true; _cue(1);
+ }
+ if( dy > 0.0 && oldY <= PADDLE_Y-RADIUS && y >= PADDLE_Y-RADIUS &&
+ paddle-PADDLE_HALF-RADIUS <= x <= paddle+PADDLE_HALF+RADIUS ) _rebound();
+ if( y > BOTTOM+RADIUS ) {
+ _lostBall();
+ break;
+ }
+ // Map the center into the grid of radius-expanded brick boxes.
+ // Their overlap is smaller than one cell: at most 2 x 2 candidates.
+ if( x <= LEFT+2 -RADIUS || x >= LEFT+2 +(COLS-1)*BRICK_DX+BRICK_W+RADIUS ||
+ y <= BRICK_TOP-RADIUS || y >= BRICK_TOP+(ROWS-1)*BRICK_DY+BRICK_H+RADIUS )
+ continue;
+ int col = (x-(LEFT+2 -RADIUS))/BRICK_DX;
+ int row = (y-(BRICK_TOP-RADIUS))/BRICK_DY;
+ bool hit = false;
+ for( int r = row > 0 ? row-1 : 0; r <= row && r < ROWS && !hit; r++ )
+ for( int c = col > 0 ? col-1 : 0; c <= col && c < COLS; c++ ) {
+ int i = r*COLS+c;
+ if( bricks[i] == 0 ) continue;
+ f64 left = LEFT+2 +c*BRICK_DX-RADIUS;
+ f64 top = BRICK_TOP+r*BRICK_DY-RADIUS;
+ f64 right = left+BRICK_W+2*RADIUS;
+ f64 bottom= top +BRICK_H+2*RADIUS;
+ if( left < x < right && top < y < bottom ) {
+ bool back = false;
+ // Resolve the face crossed this step, using the previous center.
+ // Vertical faces win a simultaneous corner contact.
+ if( oldY <= top && dy > 0.0 ) {
+ y = top; dy = -dy; back = true;
+ } else if( oldY >= bottom && dy < 0.0 ) {
+ y = bottom; dy = -dy;
+ } else if( oldX <= left && dx > 0.0 ) {
+ x = left; dx = -dx;
+ } else if( oldX >= right && dx < 0.0 ) {
+ x = right; dx = -dx;
+ } else continue; // Missed all bricks
+ _brick(i,back);
+ hit = true;
+ break;
+ }
+ }
+ }
+ return 0;
+ };
+
+
+ // Several sound things might happen in this event, pick one
+ int sound = 0; // Increasing priority: wall, paddle, brick, boost, miss, win.
+ val _cue = { int snd -> if( snd > sound ) sound = snd; };
+
+ // Ball speed is a fraction of deepest brick hit, plus backwall boost
+ f64 speed;
+ val _speed = { -> speed = (320.0+35.0*(depth-1))*(boosted ? 1.65 : 1.0); };
+
+ // Clamp paddle to wall sides
+ f64 paddle;
+ val _paddle = { f64 x ->
+ paddle = x < LEFT +PADDLE_HALF ? LEFT +PADDLE_HALF :
+ x > RIGHT-PADDLE_HALF ? RIGHT-PADDLE_HALF :
+ x;
+ };
+
+ // Hit the paddle
+ val _rebound = { ->
+ // Where along the paddle did you hit?
+ f64 offset = (x-paddle)/PADDLE_HALF;
+ if( offset < -1.0 ) offset = -1.0;
+ if( offset > 1.0 ) offset = 1.0;
+ dx = 0.88*offset;
+ // A near-center hit still drifts sideways instead of repeating forever.
+ if( -0.12 < dx < 0.12 ) dx = dx < 0.0 ? -0.12 : 0.12;
+ dy = -_sqrt(1.0-dx*dx); // Bounce upwards speed
+ y = PADDLE_Y-RADIUS; // Reset ball hit to top of paddle
+ backWall = false; // Did not hit back wall
+ _cue(2); // Paddle bounce sound
+ };
+
+ // Hit a brick?
+ int soundDepth; // Which brick depth for the boop
+ val _brick = { int index, bool back ->
+ if( bricks[index] == 0 ) return 0; // No brick to hit!
+ bricks[index] = 0; // Clear brick
+ int row = ROWS-index/COLS; // Get brick row
+ score += row; // Score by row
+ remaining--; // Fewer bricks to iwn
+ if( row > depth ) depth = row; // Max depth seen
+ // Brick-hit sound
+ if( sound <= 3 ) { _cue(3); soundDepth = row; }
+ if( back && backWall && !boosted ) { _cue(3); boosted = true; }
+ if( remaining == 0 ) { _cue(6); phase = WON; }
+ _speed();
+ return 0;
+ };
+
+ // Missed ball with paddle
+ val _lostBall = { ->
+ lives--;
+ phase = lives == 0 ? LOST : READY;
+ backWall = false;
+ depth = 1;
+ boosted = false;
+ _speed();
+ x = paddle; y = PADDLE_Y-RADIUS-6;
+ _cue(5);
+ };
+
+ // Reset for another round
+ val _reset = { ->
+ for( int i = 0; i < bricks#; i++ ) bricks[i] = 1;
+ score = 0;
+ remaining = COLS*ROWS;
+ lives = 3;
+ depth = 1;
+ phase = READY;
+ boosted = false;
+ backWall = false;
+ paused = false;
+ sound = 0;
+ soundDepth = 1;
+ paddle = 504.0;
+ x = paddle;
+ y = PADDLE_Y-RADIUS-6;
+ _speed();
+ };
+
+ // --------------------------------
+ // Drawing code
+ val _hud = { !sys.draw.Canvas c ->
+ c.text(24,22,"BREAKOUT",5,WHITE);
+ c.text(264,20,"SCORE",2,MUTED);
+ _number(c,264,40,score,3,4,WHITE);
+ c.text(424,20,"BRICKS",2,MUTED);
+ _number(c,424,40,remaining,3,4,WHITE);
+ c.text(584,20,"BALLS",2,MUTED);
+ for( int i = 0; i < lives; i++ ) c.fillCircle(590+23*i,50,5,CYAN);
+ c.text(714,20,"SPEED",2,MUTED);
+ int ispeed = speed;
+ _number(c,714,40,ispeed,3,4,boosted ? COLORS[0] : WHITE);
+ c.text(838,40,boosted ? "BOOST!" : "NORMAL",3,boosted ? COLORS[0] : MUTED);
+ };
+
+
+ val _bricks = { !sys.draw.Canvas c ->
+ c.fillRect(LEFT-4,TOP-4,RIGHT-LEFT+8,4,MUTED);
+ c.fillRect(LEFT-4,TOP,4,BOTTOM-TOP,0x29435a);
+ c.fillRect(RIGHT,TOP,4,BOTTOM-TOP,0x29435a);
+ c.text(376,108,boosted ? "BACK WALL BOOST ACTIVE" : "BREAK THROUGH FOR A BOOST",2,boosted ? COLORS[0] : MUTED);
+ for( int i = 0; i < bricks#; i++ ) {
+ if( bricks[i] == 0 ) continue;
+ int row = i/COLS;
+ i64 x = LEFT+2+(i-row*COLS)*BRICK_DX, y = BRICK_TOP+row*BRICK_DY;
+ c.fillRect(x,y,BRICK_W,BRICK_H,COLORS[row]);
+ c.fillRect(x,y,BRICK_W,2,WHITE);
+ }
+ };
+
+ val _overlay = { !sys.draw.Canvas c ->
+ if( phase != PLAY || paused ) {
+ c.fillRect(246,422,516,108,0x17263a).rect(246,422,516,108,2,0x29435a);
+ u8[~] title = paused ? "PAUSED" : phase == WON ? "WALL CLEARED!" : phase == LOST ? "GAME OVER" : "READY?";
+ c.text(504-(title#*4-1)*4/2,442,title,4,WHITE);
+ u8[~] help = paused ? "P TO CONTINUE" : phase == WON || phase == LOST ? "R TO PLAY AGAIN" : "SPACE OR CLICK TO SERVE";
+ c.text(504-(help#*4-1)*2/2,490,help,2,CYAN);
+ }
+ return 0;
+ };
+
+ // With this Canvas, we draw into the blank canvas, then "present". The
+ // underlying graphics lib copies the bits, and we are then free to clear
+ // and draw again while the old bits continue to be presented.
+ val _draw = { !sys.draw.Canvas c ->
+ c.clear(BACKGROUND);
+ _hud(c);
+ _bricks(c);
+ i64 paddle = paddle, ballX = x, ballY = y;
+ int glowRadius = RADIUS+3;
+ c.fillRect(paddle-PADDLE_HALF,PADDLE_Y,2*PADDLE_HALF,12,CYAN);
+ c.fillRect(paddle-PADDLE_HALF+4,PADDLE_Y+2,2*PADDLE_HALF-8,2,WHITE);
+ c.fillCircle(ballX,ballY,glowRadius,boosted ? 0x672a42 : 0x224b61);
+ c.fillCircle(ballX,ballY,RADIUS,boosted ? COLORS[0] : WHITE);
+ _overlay(c);
+ c.text(24,738,"ARROWS / MOUSE: MOVE SPACE / CLICK: SERVE P: PAUSE R: RESTART Q / ESC: QUIT",2,MUTED);
+ };
+
+ // Served and still holding serve key
+ bool serveHeld;
+ // Paused and still holding pause key
+ bool pauseHeld;
+ // Restart and still holding restart key
+ bool restartHeld;
+ // Last mouse position; track moving mouse
+ int lastMouse;
+
+ // Read and act on the controls
+ val _input = { ->
+ val gfx = sys.gfx;
+ bool serve = gfx.key(' ') || gfx.button(gfx.LEFT);
+ bool pause = gfx.key('P');
+ bool restart= gfx.key('R');
+
+ // Keyboard direction and mouse position
+ int direction = (gfx.key(KEY_RIGHT) ? 1 : 0)-(gfx.key(KEY_LEFT) ? 1 : 0);
+ int mouse = gfx.mouseX();
+
+ // Restart requested
+ restartHeld = debounce( restart, restartHeld, { -> _reset(); sys.audio.stop(); } );
+
+ // Playing or ready to play game?
+ if( phase == READY || phase == PLAY ) {
+ // Flip pause state
+ pauseHeld = debounce( pause, pauseHeld, { -> paused = !paused; } );
+
+ // If no key and mouse moved, set paddle to mouse
+ if( mouse != lastMouse && direction == 0 && !paused ) {
+ _paddle(mouse*1.0); // Set paddle to mouse
+ // If READY and not PLAY, ball tracks paddle directly
+ if( phase == READY ) x = paddle;
+ }
+ lastMouse = mouse;
+
+ }
+
+ // Serve requested
+ serveHeld = debounce( serve, serveHeld, _serve );
+
+ // Return key direction, so the tick tracking can slide paddle smoothly
+ return direction;
+ };
+
+ val debounce = { bool b, bool bHeld, { int } action ->
+ if( b && !bHeld ) // Action requested and repeating
+ action(); // Do action
+ b; // Dont action again, until 'b' is released
+ };
+
+
+ // Serve the ball; give it a nominal direction and speed
+ val _serve = { ->
+ if( phase == READY && !paused ) {
+ phase = PLAY;
+ dx = 0.4;
+ dy = -_sqrt(1.0-dx*dx);
+ }
+ return 0;
+ };
+};
+
+// Fixed-width decimal labels avoid allocating new strings on every frame.
+val _number = { !sys.draw.Canvas c, i64 x, i64 y, int value, int digits, int scale, u32 color ->
+ for( int i = 0; i < digits; i++ ) {
+ c.text(x+(digits-1-i)*4*scale,y,DIGITS[value-value/10*10],scale,color);
+ value /= 10;
+ }
+};
+
+// Short, fading triangle waves; the array initializer builds PCM once at startup.
+val _tone = { int period ->
+ val samples = new i16[~2400]{int i ->
+ int phase = i-i/period*period;
+ int triangle = phase < period/2 ? phase : period-phase;
+ return (triangle*4-period)*6000/period*(2400-i)/2400;
+ };
+ int handle = sys.audio.load(samples);
+ return handle < 0 ? sys.audio.BOOP : handle;
+};
+
+
+// Fast low accuracy sqrt
+val _sqrt = { f64 n ->
+ f64 r = 1.0;
+ for( int i = 0; i < 8; i++ ) r = (r+n/r)*0.5;
+ return r;
+};
+
+val main = { u8[~][] args ->
+ // Shortcuts
+ val gfx = sys.gfx, audio = sys.audio;
+
+ // Open a window
+ if( gfx.open("Simple Breakout",WIDTH,HEIGHT) != gfx.OK ) return 1;
+
+ // Audio setup; pre-specify a series of sounds
+ audio.open(); // Optional: a missing audio backend leaves play silent.
+ val sounds = new int[~]{0,_tone(120),_tone(80),audio.BOOP,audio.WIN,audio.LOSE,audio.WIN};
+ // Bottom to top: roughly 333..800 Hz, below the old 1000 Hz brick tone.
+ val brickSounds = new int[~ROWS]{int i -> int period = 144-i*12; _tone(period);};
+
+ // Setup drawing board
+ val c = new sys.draw.Canvas(WIDTH,HEIGHT);
+
+ // Setup game contents
+ val g = new Game;
+ g._reset();
+
+ //
+ int frames = 0, status = gfx.OK;
+ i64 last_ms = sys.time.monotonic(), pending_ms = 0, frame_ms = last_ms;
+
+ while( status == gfx.OK ) {
+ if( gfx.key('Q') || gfx.key(gfx.ESCAPE) ) break;
+ int direction = g._input();
+ i64 now_ms = sys.time.monotonic(), elapsed_ms = now_ms-last_ms;
+ last_ms = now_ms;
+ // Drop long stalls instead of teleporting the ball after a window drag.
+ pending_ms += elapsed_ms < 0 ? 0 : elapsed_ms > 64 ? 64 : elapsed_ms;
+ while( pending_ms >= STEP_ms ) { g._tick(direction); pending_ms -= STEP_ms; }
+ if( g.sound != 0 ) {
+ audio.play(g.sound == 3 ? brickSounds[g.soundDepth-1] : sounds[g.sound]);
+ g.sound = 0;
+ }
+ if( now_ms >= frame_ms ) {
+ g._draw(c);
+ status = gfx.present(c.pixels);
+ frame_ms = now_ms+FRAME_ms;
+ if( args# > 1 && ++frames == 10 ) break;
+ }
+ if( status == gfx.OK ) status = gfx.wait(STEP_ms-pending_ms);
+ }
+ audio.close(); gfx.close();
+ return status == gfx.ERROR ? 1 : 0;
+};
diff --git a/docs/chapter31/examples/BubbleSort.smp b/docs/chapter31/examples/BubbleSort.smp
new file mode 100644
index 00000000..6bdb13a9
--- /dev/null
+++ b/docs/chapter31/examples/BubbleSort.smp
@@ -0,0 +1,38 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/bubble-sort/
+//
+// Bubble sort, with reading (parsing) and writing (formatting) the data.
+
+val sort = { !sys.aryi64 nums ->
+ sys.aryi64?! more = nums;
+ while( more ) {
+ more = null;
+ for( u32 i = 0; i < nums.len-1; i++ )
+ if( nums.buf[i] > nums.buf[i+1] ) {
+ u32 next = i+1;
+ more = nums.swap(i,next);
+ }
+ }
+};
+
+// Read one process argument of the form "[ 4, 3, 2, 1]" and print it sorted.
+// args[0] is the executable name, as with native C argc/argv.
+val main = { u8[~][] args ->
+ val usage = { ->
+ sys.io.p("Usage: please provide a list of at least two integers to sort in the format \"[1, 2, 3, 4, 5]\"\n");
+ return 0;
+ };
+ if( args# != 2 ) return usage();
+
+ var nums = sys.aryi64.parseAryI64(args[1]);
+ if( !nums ) return usage();
+ if( nums.len < 2 ) return usage();
+
+ // Bubble sort array of integers
+ sort(nums);
+
+ // Print the same bracketed array-list format accepted as input.
+ nums.str().add('\n').write(1);
+ return 0;
+};
diff --git a/docs/chapter31/examples/Capitalize.smp b/docs/chapter31/examples/Capitalize.smp
new file mode 100644
index 00000000..00fdd481
--- /dev/null
+++ b/docs/chapter31/examples/Capitalize.smp
@@ -0,0 +1,23 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/capitalize/
+
+val main = { u8[~][] args ->
+ if( args# > 2 ) {
+ sys.io.p("Use quotes around multiple strings.\n");
+ return 0;
+ }
+ if( args# != 2 || args[1]# == 0 ) {
+ sys.io.p("Usage: please provide a string\n");
+ return 0;
+ }
+
+ u8[~] input = args[1];
+ var output = new u8[input# + 1];
+ u32 len = input#;
+ sys.ary.copy(output,0,input,0,len);
+ output[0] = sys.char.toupper(input[0]);
+ output[input#] = '\n';
+ sys.io.p(output);
+ return 0;
+};
diff --git a/docs/chapter31/examples/Clock.smp b/docs/chapter31/examples/Clock.smp
new file mode 100644
index 00000000..ecf0fae1
--- /dev/null
+++ b/docs/chapter31/examples/Clock.smp
@@ -0,0 +1,128 @@
+// Local clocks; click the button or press Space to switch. Q/Escape exits.
+// Any argument runs ten frames, exercising both faces for smoke tests.
+u32 GREEN = 0x62ff70, DIM = 0x123d20, BACKGROUND = 0x020b05;
+
+val main = { u8[~][] args ->
+ val gfx = sys.gfx;
+ val time = sys.time;
+ // The main Canvas
+ val screen = new sys.draw.Canvas(720,540);
+ if( gfx.open("Simple digital and analog clocks",screen.width,screen.height) != gfx.OK ) return 1;
+
+ // Button to flip clock style
+ val button = new sys.draw.Button(220,454,280,52,"ANALOG CLOCK");
+ val timers = new sys.time.Timers();
+ u32 frame_id = timers.every(33);
+ if( frame_id == 0 ) { gfx.close(); return 1; }
+
+ bool analog = false, held = false;
+ int frames = 0, status = gfx.OK;
+
+ // Main event loop
+ while( status == gfx.OK ) {
+ // Did the user press space bar?
+ bool space = gfx.key(' ');
+ // Did the left mouse button go down?
+ bool down = gfx.button(gfx.LEFT);
+ // ANd did it go down over this button?
+ bool click = button.update(gfx.mouseX(),gfx.mouseY(),down);
+ // If clicked, flip the clock mode
+ if( click || (space && !held) ) analog = !analog;
+ held = space;
+ if( gfx.key('Q') ) break;
+ // Change button label
+ button.label = analog ? "DIGITAL CLOCK" : "ANALOG CLOCK";
+ // If the next timer event is the every-33-msec event
+ if( timers.poll(time.monotonic()) == frame_id ) {
+ // Draw the clock screen and button
+ _draw(screen,time.now(),analog);
+ button.draw(screen,3,GREEN,BACKGROUND,DIM);
+ // Presne the canvas on screen
+ status = gfx.present(screen.pixels);
+ // Testing only: show 5 frames, flip to analog, show 5 more and exit
+ if( args# > 1 ) {
+ frames++;
+ if( frames == 5 ) analog = true;
+ if( frames == 10 ) break;
+ }
+ }
+ if( status != gfx.OK ) break;
+ // Stall until next timer event
+ status = gfx.wait(time.remaining(timers.nextDeadline(),time.monotonic()));
+ }
+ gfx.close();
+ return status == gfx.ERROR ? 1 : 0;
+};
+
+val _draw = { !sys.draw.Canvas screen, i64 wall_ms, i64 analog ->
+ val d = sys.time.local(wall_ms);
+ screen.clear(BACKGROUND);
+ screen.text(266,24,"SIMPLE CLOCK",4,GREEN);
+ if( analog ) _analog(screen,d.hour,d.minute,d.second,d.millisecond);
+ else _digital(screen,d.hour,d.minute,d.second);
+ screen.text(298,412,"%(sys.time.formatLocal(wall_ms,sys.time.DATE))",4,GREEN);
+ screen.text(249,520,"SPACE SWITCH Q OR ESC QUIT",2,GREEN);
+};
+
+// a..g bits: top, upper-right, lower-right, bottom, lower-left, upper-left,
+// middle. Faint unlit bars retain the old LED figure-eight silhouette.
+val _digit = { !sys.draw.Canvas c, int x, int y, i64 digit ->
+ int bits = digit == 0 ? 0x3f : digit == 1 ? 0x06 : digit == 2 ? 0x5b :
+ digit == 3 ? 0x4f : digit == 4 ? 0x66 : digit == 5 ? 0x6d :
+ digit == 6 ? 0x7d : digit == 7 ? 0x07 : digit == 8 ? 0x7f : digit == 9 ? 0x6f : 0;
+ for( int i = 0; i < 7; i++ ) {
+ u32 color = (bits&(1<<i)) != 0 ? GREEN : DIM;
+ if( i == 0 || i == 3 || i == 6 ) {
+ int dy = i == 0 ? 0 : i == 3 ? 144 : 72;
+ c.line(x+14,y+dy,x+58,y+dy,10,color);
+ } else {
+ bool right = i == 1 || i == 2;
+ int dx = right ? 68 : 4, dy = i == 1 || i == 5 ? 12 : 84;
+ c.line(x+dx,y+dy,x+dx,y+dy+48,10,color);
+ }
+ }
+};
+
+val _digital = { !sys.draw.Canvas c, int hour, int minute, int second ->
+ _digit(c,68,166,hour/10); _digit(c,154,166,hour-hour/10*10);
+ _digit(c,284,166,minute/10); _digit(c,370,166,minute-minute/10*10);
+ _digit(c,500,166,second/10); _digit(c,586,166,second-second/10*10);
+ for( int i = 0; i < 2; i++ ) {
+ c.fillCircle(254+i*216,214,6,GREEN);
+ c.fillCircle(254+i*216,262,6,GREEN);
+ }
+ c.text(272,352,"LOCAL TIME 24H",3,GREEN);
+};
+// Rational sine approximation, accurate enough for pixel clock geometry.
+// Arguments are nonnegative turns; no additional native math dependency.
+val _sin = { f64 turn ->
+ i64 whole = turn; turn -= whole;
+ bool negative = turn > 0.5;
+ f64 x = (negative ? turn-0.5 : turn)*6.283185307179586;
+ f64 product = x*(3.141592653589793-x);
+ f64 numerator = negative ? -16.0 : 16.0;
+ return numerator*product/(49.34802200544679-4*product);
+};
+val _hand = { !sys.draw.Canvas c, f64 turn, int length, int thickness, u32 color ->
+ int x = 360+_sin(turn)*length, y = 232-_sin(turn+0.25)*length;
+ c.line(360,232,x,y,thickness,color);
+};
+val _analog = { !sys.draw.Canvas c, int hour, int minute, int second, int millisecond ->
+ c.circle(360,232,154,3,GREEN);
+ for( int i = 0; i < 60; i++ ) {
+ f64 turn = i/60.0;
+ bool major = i-i/5*5 == 0;
+ int inner = major ? 133 : 143;
+ int x0 = 360+_sin(turn)*inner, y0 = 232-_sin(turn+0.25)*inner;
+ int x1 = 360+_sin(turn)*149, y1 = 232-_sin(turn+0.25)*149;
+ c.line(x0,y0,x1,y1,major ? 3 : 1,GREEN);
+ }
+ c.text(349,100,"12",2,GREEN); c.text(484,227,"3",2,GREEN);
+ c.text(357,354,"6",2,GREEN); c.text(230,227,"9",2,GREEN);
+ f64 seconds = second+millisecond/1000.0;
+ f64 minutes = minute+seconds/60.0;
+ _hand(c,(hour-hour/12*12+minutes/60.0)/12.0,78,8,GREEN);
+ _hand(c,minutes/60.0,112,5,GREEN);
+ _hand(c,seconds/60.0,128,2,0xc3ffcb);
+ c.fillCircle(360,232,6,GREEN);
+};
diff --git a/docs/chapter31/examples/Dijkstra.smp b/docs/chapter31/examples/Dijkstra.smp
new file mode 100644
index 00000000..6591b310
--- /dev/null
+++ b/docs/chapter31/examples/Dijkstra.smp
@@ -0,0 +1,71 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/dijkstra/
+
+val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst ->
+ i64 infinity = sys.integer.MAX_I64;
+ var visited = new sys.adt.bitset();
+ // All nodes are infinitely far from each other, except the starting point
+ var dist = new i64[nodes]{ int i -> i == src ? 0 : infinity; };
+
+ i64 result = -1;
+ int count = 0;
+ while( count < nodes && result < 0 ) {
+ int nearest = -1;
+ i64 best = infinity;
+ for( int node = 0; node < nodes; node++ )
+ if( !visited.get(node) && dist[node] < best ) {
+ nearest = node;
+ best = dist[node];
+ }
+ if( nearest < 0 ) count = nodes;
+ else {
+ if( nearest == dst ) return best;
+ visited.set(nearest);
+ for( int node = 0; node < nodes; node++ ) {
+ u32 index = nearest*nodes + node;
+ i64 weight = matrix.get(index);
+ if( weight > 0 && !visited.get(node) && best+weight < dist[node] )
+ dist[node] = best+weight;
+ }
+ }
+ count++;
+ }
+ return result;
+};
+
+val usage = { ->
+ sys.io.p("Usage: please provide three inputs: a serialized matrix, a source node and a destination node\n");
+ return 0;
+};
+
+val main = { u8[~][] args ->
+ // Exactly 3 args
+ if( args# != 4 ) return usage();
+ // First is a serialized square matrix of positive numbers
+ var matrix = sys.aryi64.parseAryI64(args[1]);
+ if( !matrix ) return usage();
+ if( !matrix.len ) return usage();
+ // All numbers are positive
+ for( u32 i = 0; i < matrix.len; i++ )
+ if( matrix.get(i) < 0 ) return usage();
+ // Matrix is square
+ int nodes = 0;
+ while( nodes*nodes < matrix.len ) nodes++;
+ if( nodes*nodes != matrix.len )
+ return usage(); // Not a square matrix
+
+ // src,dst are values within the matrix
+ i64 src = sys.aryu8.atoi(args[2]);
+ i64 dst = sys.aryu8.atoi(args[3]);
+ if( src < 0 || src >= nodes || dst < 0 || dst >= nodes )
+ return usage(); // Source and destination land within the graph
+
+ int source = src, destination = dst;
+ i64 cost = shortestPath(matrix,nodes,source,destination);
+ if( cost >= 0 ) {
+ new sys.aryu8().num(cost).add('\n').write(1);
+ return 0;
+ }
+ return usage();
+};
diff --git a/docs/chapter31/examples/FileIO.smp b/docs/chapter31/examples/FileIO.smp
new file mode 100644
index 00000000..59b3159e
--- /dev/null
+++ b/docs/chapter31/examples/FileIO.smp
@@ -0,0 +1,52 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/file-input-output/
+
+val FILE = "output.txt";
+
+val main = { u8[~][] args ->
+ var str = new sys.aryu8();
+ str.addAry("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n");
+ str.addAry("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n");
+ str.addAry("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n");
+ str.addAry("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n");
+
+ // Create
+ i32 fd = sys.io.create(FILE);
+ if( fd == -1 ) return _error();
+ // Write
+ u32 len = str.len;
+ int wrote = sys.io.writeOff(fd,str.buf,0,len);
+ if( wrote == -1 ) return _error();
+ if( wrote != str.len ) return _error2(-2);
+ // Close
+ if( sys.io.close(fd) != 0 ) return _error();
+
+ // Open
+ i32 fd2 = sys.io.open(FILE);
+ if( fd2 == -1 ) return _error();
+ // Read
+ var got = sys.io.read(fd2);
+ if( !got ) return _error();
+ if( got# != str.len ) return _error2(-3);
+ // Close
+ if( sys.io.close(fd2) != 0 ) return _error();
+
+ // Check contents are equal
+ if( sys.ary.cmp(got,0,str.buf,0,len) )
+ return _error2(-4);
+
+ // Remove
+ if( sys.io.remove(FILE) != 0 ) return _error();
+
+ sys.io.p("File I/O succeeded\n");
+ return 0;
+};
+
+val _error2 = { i32 errno ->
+ sys.io.remove(FILE);
+ new sys.aryu8().addAry("Failed with error code ").num(errno).add('\n').write(sys.libc.STDERR);
+ sys.libc._exit(errno);
+};
+
+val _error = { -> _error2(sys.libc.errno()); };
diff --git a/docs/chapter31/examples/Graphics.smp b/docs/chapter31/examples/Graphics.smp
new file mode 100644
index 00000000..8b251fa2
--- /dev/null
+++ b/docs/chapter31/examples/Graphics.smp
@@ -0,0 +1,17 @@
+// Optional graphics smoke test: pixels are drawn entirely in Simple.
+// Pass any argument for a short, self-closing test; otherwise close or press Esc.
+val main = { u8[~][] args ->
+ if( sys.gfx.open("Simple pixels",320,240) != sys.gfx.OK ) return 1;
+ val pixels = new u32[320*240];
+ for( int y = 0; y < 240; y++ )
+ for( int x = 0; x < 320; x++ )
+ pixels[y*320+x] = (x*255/319 << 16) | (y*255/239 << 8) | 64;
+ int result = sys.gfx.present(pixels);
+ int frames = 0;
+ while( result == sys.gfx.OK ) {
+ result = sys.gfx.poll(true);
+ if( args# > 1 && ++frames == 5 ) break;
+ }
+ sys.gfx.close();
+ return result == sys.gfx.ERROR ? 1 : 0;
+};
diff --git a/docs/chapter31/examples/README.md b/docs/chapter31/examples/README.md
new file mode 100644
index 00000000..20b644cf
--- /dev/null
+++ b/docs/chapter31/examples/README.md
@@ -0,0 +1,108 @@
+# Runnable examples
+
+These programs live beside the documentation so readers can find and run them.
+Their non-graphical native regression tests run as part of `make tests`.
+The optional [pixel-window example](Graphics.smp) uses MiniFB; see
+[graphics setup](../graphics.md). `make run-graphics` opens it explicitly.
+The [graphical tic-tac-toe](TicTacToeGraphical.smp) uses the Simple
+[drawing library](../drawing.md). Run it with
+`make run-graphical-tictactoe CTAGS=`; click a cell or press 1–9, R/Space to
+replay, and Q/Escape to quit. The terminal game below remains available.
+Optional [background audio](../audio.md) supplies soft move and win/loss cues.
+
+The [digital and analog clock](Clock.smp) demonstrates the [time library](../time.md),
+green seven-segment digits, smooth clock hands, and a reusable software button.
+Run `make run-clock CTAGS=`; click the button or press Space to switch faces,
+and Q/Escape to exit. Both faces show system-local time.
+`make clock-example CTAGS=` builds it without starting a window.
+
+The [Breakout game](Breakout.smp) has 24 columns and 8 rows, with colors and
+point values by depth: 1 at the bottom through 8 at the top, 864 points total.
+Clear the wall before losing all three balls. Mouse movement or the arrow keys
+move the paddle; Space/left click serves, P pauses, R restarts, Q/Escape exits.
+Run `make run-breakout CTAGS=` or build only with `make breakout CTAGS=`.
+The familiar paddle, serve, and limited-ball rules follow
+[Atari's Breakout instructions](https://atari.com/pages/breakout).
+
+The deepest row reached with the current ball sets the base speed: 320 pixels/second plus 35 for
+each additional depth. After touching the back wall, hitting the top of a brick
+activates a **one-time 1.65x speed boost**; side/front hits do not. Returning to
+the paddle disarms that trip's back-wall opportunity. Losing a ball resets depth
+and boost, so each new ball starts at 320 pixels/second and can earn its own
+back-wall boost. Edge hits on the paddle give a wider
+rebound than center hits. Clear all bricks to win; no new wall replaces them.
+
+Physics uses fixed 8 ms ticks with movement split into steps of at most two
+pixels, including at boosted speed. Rendering runs roughly every 16 ms. The
+brick and sound tables demonstrate array initializers; the short bounce tones
+use read-only PCM arrays made by index functions. Brick boops rise in pitch with
+row depth, from about 333 Hz at the bottom to 800 Hz at the top. Audio is optional. Native
+tests in `BreakoutTest` exercise collisions, scoring, lives, pause, paddle angles,
+the boost, and clearing the whole wall without a display or audio device.
+
+From `chapter31`, build the tic-tac-toe executable (without starting it):
+
+```sh
+make lib tictactoe CTAGS=
+```
+
+Run the Windows build from PowerShell:
+
+```powershell
+.\build\examples\x86_64_v2_win64\TicTacToe.exe
+```
+
+Or from Cygwin:
+
+```sh
+./build/examples/x86_64_v2_win64/TicTacToe.exe
+```
+
+The Windows executable uses Cygwin's runtime; its `bin` directory must be on
+`PATH` (as it is when building with Cygwin GCC).
+
+For a native Linux build, run these commands from this same `chapter31`
+directory in WSL. Separate Java and native output directories let the Windows
+and Linux builds coexist:
+
+```sh
+make lib tictactoe CLZDIR=build/classes-linux CTAGS=
+./build/examples/x86_64_v2_SystemV/TicTacToe
+```
+
+From PowerShell, prefix either command with `wsl -d Ubuntu-24.04 --`.
+On a standalone Linux checkout, `CLZDIR` can be omitted.
+The compiler's `--link` option produces an executable without running it;
+`--norun` continues to produce object files only.
+
+`TicTacToe.smp` currently prints the welcome, square numbering, and empty board,
+then waits for a move. Press **1–9** without Enter to place X, or
+**q**, Escape, Ctrl-C, or Ctrl-D to quit. Enter is silently ignored; other
+invalid keys and occupied squares prompt again without taking a turn. The board
+is printed after each player and CPU move; only accepted move digits are echoed.
+Play continues until someone wins or the board is
+full. The CPU takes an immediate win, blocks an immediate loss, then prefers
+the center, corners, and edges.
+
+Other examples are Hello World (`A_helloWorld.smp`), `BubbleSort.smp`,
+`Capitalize.smp`, `Dijkstra.smp`, and `FileIO.smp`. The latter creates, checks,
+and removes `output.txt` in the working directory.
+
+## Single-keystroke input
+
+`sys.io.getch()` blocks for one byte without terminal editing or echo. It returns
+0–255 for a byte, or -1 for EOF or an error. On -1, `sys.libc.errno()` is
+`sys.libc.EOF` (-1) for end-of-input, otherwise the OS error code.
+Terminal Ctrl-C and Ctrl-D are bytes 3
+and 4, so the application decides how to handle them. Unicode and function keys
+may produce multiple bytes; this is an ASCII-oriented byte API, not a key-event
+decoder.
+
+The small C runtime adapter uses [POSIX terminal modes](https://man7.org/linux/man-pages/man3/termios.3.html),
+including Cygwin on Windows. It restores the saved mode before returning,
+including after an interrupted read. Files and pipes use an ordinary one-byte
+read. `make test CLASS=TerminalInputTest` runs the Java regression tests for
+redirected input, errors, prompt output, Enter, and Escape. Check terminal
+editing, echo, and mode restoration by running the executable in a terminal.
+`make test CLASS=TicTacToeTest` checks complete games, all eight winning lines,
+draws, occupied squares, and CPU blocking.
diff --git a/docs/chapter31/examples/TicTacToe.smp b/docs/chapter31/examples/TicTacToe.smp
new file mode 100644
index 00000000..ee4e3182
--- /dev/null
+++ b/docs/chapter31/examples/TicTacToe.smp
@@ -0,0 +1,132 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+val main = { u8[~][] args ->
+ sys.io.p("Tic-tac-toe!\nYou are X; the CPU is O. You go first.\n");
+ sys.io.p("Press a square's number (no Enter needed), or q/Esc to quit.\n");
+ sys.io.p("Squares:\n 1 | 2 | 3 \n---+---+---\n 4 | 5 | 6 \n---+---+---\n 7 | 8 | 9 \n\n");
+ val board = new u8[9]{ int i -> ' '; }; // Initialize the board
+ // Draw empty board
+ _draw(board);
+
+ while( true ) {
+ sys.io.p("Your move (1-9): ");
+ // Valid user move 1-9, or 0 for quitting
+ int square = _validMove(board) - 1;
+ if( square < 0 ) return 0;
+ // Set user choice; print board
+ board[square] = 'X';
+ sys.io.p("\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ // Compute CPU move
+ int reply = _cpu(board);
+ // Set CPU choice; print board
+ board[reply] = 'O';
+ sys.io.p("CPU plays %(reply+1).\n");
+ _draw(board);
+ // A winner?
+ if( _finished(board) ) return 0;
+ }
+};
+
+// Return with a valid user move 1-9, or 0 to exit
+val _validMove = { u8[~] board ->
+ while( true ) {
+ int key = sys.io.getch();
+ if( key == -1 && sys.libc.errno() != sys.libc.EOF ) {
+ sys.io.err("\nUnable to read the keyboard.\n");
+ return 0;
+ }
+ // Various quitting conditions
+ if( key == -1 || key == 'q' || key == 'Q' || key == 27 || key == 3 || key == 4 ) {
+ sys.io.p("\nGoodbye!\n");
+ return 0;
+ }
+ // Ignore CR/LF
+ if( key == '\r' || key == '\n' ) continue;
+
+ // Valid choice
+ if( key < '1' || key > '9' ) {
+ sys.io.p("\nChoose a square from 1 to 9: ");
+ continue;
+ }
+ if( board[key-'1'] != ' ' ) {
+ sys.io.p("\nThat square is occupied. Your move (1-9): ");
+ continue;
+ }
+ sys.io.p("%c(key)");
+ return key-'0';
+ }
+};
+
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ for( int i = 0; i < 3; i++ ) {
+ int row = i*3;
+ if( board[row] != ' ' && board[row] == board[row+1] && board[row] == board[row+2] )
+ return board[row];
+ if( board[i] != ' ' && board[i] == board[i+3] && board[i] == board[i+6] )
+ return board[i];
+ }
+ if( board[4] != ' ' ) {
+ if( board[0] == board[4] && board[4] == board[8] ) return board[4];
+ if( board[2] == board[4] && board[4] == board[6] ) return board[4];
+ }
+ return 0;
+};
+
+val _finished = { u8[~] board ->
+ int winner = _winner(board);
+ if( winner == 'X' ) { sys.io.p("You win!\n"); return true; }
+ if( winner == 'O' ) { sys.io.p("CPU wins!\n"); return true; }
+ for( int i = 0; i < board#; i++ )
+ if( board[i] == ' ' ) return false;
+ sys.io.p("It's a draw!\n");
+ return true;
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i = 0; i < board#; i++ ) {
+ if( board[i] == ' ' ) {
+ board[i] = mark;
+ bool wins = _winner(board) == mark;
+ board[i] = ' '; // Restore board
+ if( wins ) return i;
+ }
+ }
+ return -1;
+};
+
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win >= 0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block >= 0 ) return block;
+ // If center is open, take it
+ if( board[4] == ' ' ) return 4;
+ // If a corner is open, take it
+ for( int i = 0; i < board#; i += 2 )
+ if( board[i] == ' ' ) return i;
+ // Take whatever is available
+ for( int i = 0; i < board#; i++ )
+ if( board[i] == ' ' ) return i;
+ return -1;
+};
+
+val _draw = { u8[~] board ->
+ val out = new sys.aryu8();
+ for( int row = 0; row < 3; row++ ) {
+ for( int col = 0; col < 3; col++ ) {
+ out.add(' ').add(board[row*3+col]).add(' ');
+ if( col < 2 ) out.add('|');
+ }
+ out.add('\n');
+ if( row < 2 ) out.addAry("---+---+---\n");
+ }
+ out.write(sys.libc.STDOUT);
+};
diff --git a/docs/chapter31/examples/TicTacToeGraphical.smp b/docs/chapter31/examples/TicTacToeGraphical.smp
new file mode 100644
index 00000000..d3d575a7
--- /dev/null
+++ b/docs/chapter31/examples/TicTacToeGraphical.smp
@@ -0,0 +1,168 @@
+// Player is X, CPU is O. Squares are numbered left-to-right, top-to-bottom.
+// Graphical tic-tac-toe; the same strategy as TicTacToe.smp.
+// Click an empty square or press 1-9. R/Space replays; Q/Escape quits.
+// Pass any argument for a five-poll window smoke test.
+val main = { u8[~][] args ->
+ val screen = new sys.draw.Canvas(600,780);
+ val board = new u8[9];
+ _reset(board);
+ if( sys.gfx.open("Simple tic-tac-toe",screen.width,screen.height) != sys.gfx.OK ) return 1;
+ sys.audio.open(); // Audio is optional; failure leaves the game silent.
+ _draw(screen,board,-1);
+ int result = sys.gfx.present(screen.pixels);
+ bool mouseHeld = false, keyHeld = false, replayHeld = false;
+ int hover = -1, frames = 0;
+
+ while( result == sys.gfx.OK ) {
+ result = sys.gfx.wait(17);
+ if( result != sys.gfx.OK ) break;
+ if( sys.gfx.key('Q') ) break;
+ bool down = sys.gfx.button(sys.gfx.LEFT);
+ bool replay = sys.gfx.key('R') || sys.gfx.key(' ');
+ int square = _square(sys.gfx.mouseX(),sys.gfx.mouseY());
+ bool dirty = square != hover;
+ hover = square;
+ if( replay && !replayHeld ) { sys.audio.stop(); _reset(board); dirty = true; }
+ bool digit = false;
+ int choice = down && !mouseHeld ? square : -1;
+ for( int i = 0; i < 9; i++ ) {
+ int key = '1'+i;
+ if( sys.gfx.key(key) ) {
+ digit = true;
+ if( !keyHeld ) choice = i;
+ }
+ }
+ if( !(replay && !replayHeld) && _move(board,choice) ) {
+ sys.audio.play(_sound(board));
+ dirty = true;
+ }
+ mouseHeld = down;
+ keyHeld = digit;
+ replayHeld = replay;
+ if( dirty ) {
+ _draw(screen,board,hover);
+ result = sys.gfx.present(screen.pixels);
+ }
+ if( args# > 1 && ++frames == 5 ) break;
+ }
+ sys.audio.close();
+ sys.gfx.close();
+ return result == sys.gfx.ERROR ? 1 : 0;
+};
+
+// Called only for an accepted move; invalid input never requests a sound.
+val _sound = { u8[~] board ->
+ int winner = _winner(board);
+ return winner == 'X' ? sys.audio.WIN : winner == 'O' ? sys.audio.LOSE : sys.audio.BOOP;
+};
+
+val _reset = { u8[] board ->
+ for( int i = 0; i < 9; i++ ) board[i] = ' ';
+};
+val _square = { int x, int y ->
+ if( x < 48 || x >= 552 || y < 168 || y >= 672 ) return -1;
+ return (y-168)/168*3+(x-48)/168;
+};
+
+// One player turn followed by one CPU turn. Invalid/finished moves do nothing.
+val _move = { u8[] board, int square ->
+ if( square < 0 || square >= 9 || board[square] != ' ' || _finished(board) ) return false;
+ board[square] = 'X';
+ if( !_finished(board) ) {
+ int reply = _cpu(board);
+ if( reply >= 0 ) board[reply] = 'O';
+ }
+ return true;
+};
+
+val _finished = { u8[~] board ->
+ if( _winner(board) ) return true;
+ for( int i = 0; i < 9; i++ ) if( board[i] == ' ' ) return false;
+ return true;
+};
+
+val _winningLine = { u8[~] board ->
+ for( int i = 0; i < 3; i++ ) {
+ int row = i*3;
+ if( board[row] != ' ' && board[row] == board[row+1] && board[row] == board[row+2] ) return i;
+ if( board[i] != ' ' && board[i] == board[i+3] && board[i] == board[i+6] ) return 3+i;
+ }
+ if( board[4] != ' ' ) {
+ if( board[0] == board[4] && board[4] == board[8] ) return 6;
+ if( board[2] == board[4] && board[4] == board[6] ) return 7;
+ }
+ return -1;
+};
+// Return the winning mark, or zero.
+val _winner = { u8[~] board ->
+ int line = _winningLine(board);
+ return line < 0 ? 0 : line < 3 ? board[line*3] : line < 6 ? board[line-3] : board[4];
+};
+
+// Try each empty square, restoring it before returning.
+val _threat = { u8[] board, u8 mark ->
+ for( int i = 0; i < 9; i++ )
+ if( board[i] == ' ' ) {
+ board[i] = mark;
+ bool wins = _winner(board) == mark;
+ board[i] = ' '; // Restore board
+ if( wins ) return i;
+ }
+ return -1;
+};
+// Win if possible, otherwise block, then prefer center, corners, and edges.
+// Called only after _finished has ruled out a full board.
+val _cpu = { u8[] board ->
+ // If O can win, do so
+ int win = _threat(board,'O');
+ if( win >= 0 ) return win;
+ // If X will win next move, block
+ int block = _threat(board,'X');
+ if( block >= 0 ) return block;
+ // If center is open, take it
+ if( board[4] == ' ' ) return 4;
+ // If a corner is open, take it
+ for( int i = 0; i < 9; i += 2 ) if( board[i] == ' ' ) return i;
+ // Take whatever is available
+ for( int i = 0; i < 9; i++ ) if( board[i] == ' ' ) return i;
+ return -1;
+};
+
+val _draw = { !sys.draw.Canvas screen, u8[~] board, int hover ->
+ u32 background = 0x142e36, paper = 0xf2eddf, ink = 0x35535a;
+ u32 cross = 0xe47b57, ring = 0x248b89, accent = 0xf0c778;
+ int winner = _winner(board);
+ bool finished = _finished(board);
+ screen.clear(background);
+ screen.text(48,34,"TIC TAC TOE",6,paper);
+ screen.fillRect(48,90,504,3,ink);
+ screen.text(48,116,finished ? (winner == 'X' ? "YOU WIN!" : winner == 'O' ? "CPU WINS" : "DRAW") : "YOUR TURN",4,accent);
+ screen.line(428,112,450,134,5,cross).line(450,112,428,134,5,cross);
+ screen.circle(496,123,13,5,0x70c9bf);
+ screen.text(428,145,"YOU",2,0xa2b9b7);
+ screen.text(484,145,"CPU",2,0xa2b9b7);
+ screen.fillRect(48,168,504,504,paper);
+ if( !finished && hover >= 0 && hover < 9 && board[hover] == ' ' )
+ screen.fillRect(48+(hover-hover/3*3)*168,168+hover/3*168,168,168,0xd9e8de);
+ for( int i = 1; i < 3; i++ ) {
+ screen.fillRect(48+i*168-3,168,6,504,ink);
+ screen.fillRect(48,168+i*168-3,504,6,ink);
+ }
+ for( int i = 0; i < 9; i++ ) {
+ int cx = 132+(i-i/3*3)*168, cy = 252+i/3*168;
+ if( board[i] == 'X' )
+ screen.line(cx-38,cy-38,cx+38,cy+38,12,cross).line(cx+38,cy-38,cx-38,cy+38,12,cross);
+ else if( board[i] == 'O' ) screen.circle(cx,cy,48,11,ring);
+ else screen.text(cx-6,cy-10,i == 0 ? "1" : i == 1 ? "2" : i == 2 ? "3" :
+ i == 3 ? "4" : i == 4 ? "5" : i == 5 ? "6" : i == 6 ? "7" : i == 7 ? "8" : "9",4,0x95a59e);
+ }
+ int line = _winningLine(board);
+ if( line >= 0 ) {
+ int first = line < 3 ? line*3 : line < 6 ? line-3 : line == 6 ? 0 : 2;
+ int last = line < 3 ? first+2 : line < 6 ? first+6 : line == 6 ? 8 : 6;
+ screen.line(132+(first-first/3*3)*168,252+first/3*168,132+(last-last/3*3)*168,252+last/3*168,5,0xeab64e);
+ }
+ screen.text(48,704,"CLICK A SQUARE OR PRESS 1-9",3,paper);
+ screen.text(48,738,"R OR SPACE REPLAY Q OR ESC QUIT",3,0xa2b9b7);
+ screen.rect(48,168,504,504,3,ink);
+};