x2c

Give your game a Lisp player.

Play tic-tac-toe against a Lisp strategy inside a native x2c program.

tic-tac-toe.x / bindings and interpreter excerpts
$lisp.binding(game, "empty?")
static Var _open(int square) {
  if (_empty(square)) return <true>;
  return %();
}

$lisp.binding(game, "wins?")
static Var _wins(int square, String player) {
  if (!_empty(square)) return %();
  char saved = board[square - 1];
  board[square - 1] = player == %"X" ? 'X' : 'O';
  int won = _won(board[square - 1]);
  board[square - 1] = saved;
  if (won) return <true>;
  return %();
}

Lisp lisp = Lisp.new();
defer lisp.destroy();
$lisp.install(lisp, game);
File strategy = File.open(argv[1], "r");
defer strategy.close();
Lisp.eval_file(lisp, strategy);

// On each O turn:
square = lisp.eval(%(choose-move));
tic-tac-toe.xlisp / complete strategy
(def squares '(1 2 3 4 5 6 7 8 9))

(defun first-move (predicate choices)
  (if (null? choices)
    nil
    (if (predicate (car choices))
      (car choices)
      (first-move predicate (cdr choices)))))

(defun winning-move (player)
  (first-move (lambda (square) (wins? square player)) squares))

(defun choose-move ()
  (or (winning-move "O")
      (winning-move "X")
      (first-move empty? '(5 1 3 7 9 2 4 6 8))))

You are X. Lisp is O.

A tic-tac-toe position with X in squares 1 and 2, O in the center and square 3. O has blocked X's top row.

The binding decorator exposes empty? and wins? to Lisp. wins? tries a move and restores the square afterward, so the script can ask about the native board without keeping a second copy.

Load the opponent.

Install the bindings, load the strategy, then call choose-move on each O turn. It returns a square number; the host checks and plays it.

Choose a move.

Try to win, then block X. Otherwise, take the first free square in the preference list: center, corners, edges. or stops at the first move found.

Edit this strategy to change the opponent without rebuilding the game.

Write a better opponent.

The strategy takes a win, blocks yours, then prefers the center, corners, and edges. It can miss forks. Teach it to look further ahead by editing the Lisp file, then run the same executable again.

The complete x2c source and strategy are included here.

Your turn

Run it locally.

Build the game, then play as X. Enter a square number to move.

You need a GCC- or Clang-compatible C compiler, ar, GNU Make, Python 3, and Bash. The build guide covers setup in detail.

From a new checkout
git clone https://github.com/gwf/x2c.git
cd x2c
git checkout --detach 53c1ed815d792b4a1af2b182a63e32feb1a5e007
make build-safe

./x2c build --output /tmp/tic-tac-toe examples/magic/tic-tac-toe.x
/tmp/tic-tac-toe examples/magic/tic-tac-toe.xlisp