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-12 13:48:30 -0800
commit7824fa044ac7d2db13af5873ce2e90365153e0be (patch)
tree0795225a660a8bda2ae0b0bb6d7b013803c9775e /docs/chapter31/examples
parent2eb27046f9b2bca887fd9ea0a7c5a67547153ef3 (diff)
downloadSimple-linear-chapter31.tar.gz
Simple-linear-chapter31.zip
Chapter 31: Software Drawing and Graphical Gameslinear-chapter31linear
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/BubbleSort.smp38
-rw-r--r--docs/chapter31/examples/Capitalize.smp23
-rw-r--r--docs/chapter31/examples/Dijkstra.smp74
-rw-r--r--docs/chapter31/examples/FileIO.smp52
-rw-r--r--docs/chapter31/examples/Graphics.smp17
-rw-r--r--docs/chapter31/examples/README.md78
-rw-r--r--docs/chapter31/examples/TicTacToe.smp133
-rw-r--r--docs/chapter31/examples/TicTacToeGraphical.smp167
9 files changed, 584 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/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/Dijkstra.smp b/docs/chapter31/examples/Dijkstra.smp
new file mode 100644
index 00000000..768bdd08
--- /dev/null
+++ b/docs/chapter31/examples/Dijkstra.smp
@@ -0,0 +1,74 @@
+
+// An implementation of
+// https://sampleprograms.io/projects/dijkstra/
+
+val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst ->
+ i64 infinity = (1 << 63)-1;
+ var dist = new i64[nodes];
+ var visited = new sys.adt.bitset();
+ // All nodes are infinitely far from each other
+ for( int i=0; i<nodes; i++ ) dist[i] = infinity;
+ // Starting point is zero far from starting point
+ dist[src] = 0;
+
+ 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..7ba5c6e7
--- /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..a1d81be2
--- /dev/null
+++ b/docs/chapter31/examples/README.md
@@ -0,0 +1,78 @@
+# 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.
+
+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..a710e440
--- /dev/null
+++ b/docs/chapter31/examples/TicTacToe.smp
@@ -0,0 +1,133 @@
+// 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];
+ for( int i=0; i<board#; i++ ) board[i]=' ';
+ // 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..b4f8bcf4
--- /dev/null
+++ b/docs/chapter31/examples/TicTacToeGraphical.smp
@@ -0,0 +1,167 @@
+// 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.poll(true);
+ 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);
+};