diff --git a/.github/release-notes.md b/.github/release-notes.md
new file mode 100644
index 0000000..710a5fc
--- /dev/null
+++ b/.github/release-notes.md
@@ -0,0 +1,23 @@
+
+
+
+
+Chess for the NumWorks N0120, in a native app of about 20 KB.
+
+### Install
+
+1. Download **`chess.nwa`** below.
+2. Plug in the calculator and open [my.numworks.com/apps](https://my.numworks.com/apps) in Chrome or Edge.
+3. Upload the file. **Chess** appears at the end of the home screen.
+
+### What's inside
+
+* **Play the bots:** 12 opponents from Pip (150) to Atlas (2100). Play White, Black or a random color, with undo, flip and resign.
+* **Puzzles:** generated on the calculator, so they never run out. Modes are Mix, Mate in 1, 2 or 3, and Best move, with a rating, a streak, hints and solutions.
+* **2 Players:** a chess clock (1+0 up to 30+0, or none) and a board that turns to face whoever is to move.
+* **Full rules:** castling, en passant, underpromotion, and draws by stalemate, repetition, the 50-move rule or insufficient material.
+
+
+
+
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..f02a2b5
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,18 @@
+name: Build
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install ARM toolchain
+ run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi libnewlib-arm-none-eabi
+ - name: Engine tests
+ run: make test
+ - name: Build
+ run: make build check
+ - uses: actions/upload-artifact@v4
+ with:
+ name: chess.nwa
+ path: output/chess.nwa
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..2a6006e
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,36 @@
+name: Release
+on:
+ push:
+ tags: ['v*']
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: Tag to release, for example v1.0.0
+ required: true
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install ARM toolchain
+ run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi libnewlib-arm-none-eabi
+ - name: Test
+ run: make test
+ - name: Build
+ run: make build check
+ - name: Publish release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ TAG: ${{ github.event.inputs.tag || github.ref_name }}
+ run: |
+ if gh release view "$TAG" > /dev/null 2>&1; then
+ gh release upload "$TAG" output/chess.nwa --clobber
+ else
+ sed "s/{{TAG}}/$TAG/g" .github/release-notes.md > notes.md
+ gh release create "$TAG" output/chess.nwa --target "$GITHUB_SHA" \
+ --title "NumChess $TAG" --notes-file notes.md
+ fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd6aad7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+output/
+__pycache__/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..41ae0d3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+# NumChess — chess for the NumWorks calculator.
+# make build output/chess.nwa
+# make run install on a connected calculator
+# make check link the app like the calculator does (output/chess.bin)
+# make test run engine tests on the host
+Q ?= @
+CC = arm-none-eabi-gcc
+NWLINK = npx --yes -- nwlink@1.0.0
+BUILD_DIR = output
+SRC = src/main.c src/chess.c src/libc.c
+
+CFLAGS = -std=c99 $(shell $(NWLINK) eadk-cflags-device)
+CFLAGS += -Os -Wall -fno-math-errno -fno-tree-loop-distribute-patterns -fno-reorder-functions -flto-partition=one
+CFLAGS += -flto -fno-fat-lto-objects -fwhole-program -fvisibility=internal
+LDFLAGS = -Wl,--relocatable -nostartfiles --specs=nano.specs
+LDFLAGS += -Wl,-e,main -Wl,-u,eadk_app_name -Wl,-u,eadk_app_icon -Wl,-u,eadk_api_level
+LDFLAGS += -Wl,--gc-sections -flinker-output=nolto-rel
+
+.PHONY: build run check test clean
+build: $(BUILD_DIR)/chess.nwa
+
+run: $(BUILD_DIR)/chess.nwa
+ @echo "INSTALL $<"
+ $(Q) $(NWLINK) install-nwa $<
+
+check: $(BUILD_DIR)/chess.nwa
+ $(Q) $(NWLINK) nwa-bin $< $(BUILD_DIR)/chess.bin
+ @echo "BIN $(BUILD_DIR)/chess.bin: $$(wc -c < $(BUILD_DIR)/chess.bin) bytes"
+
+$(BUILD_DIR)/chess.nwa: $(SRC) src/chess.h src/sprites.h $(BUILD_DIR)/icon.o
+ @echo "LD $@"
+ $(Q) $(CC) $(CFLAGS) $(LDFLAGS) $(SRC) $(BUILD_DIR)/icon.o -o $@
+ $(Q) arm-none-eabi-strip --strip-unneeded $@
+ $(Q) arm-none-eabi-size $@
+
+$(BUILD_DIR)/icon.o: src/icon.png | $(BUILD_DIR)
+ @echo "ICON $<"
+ $(Q) $(NWLINK) png-icon-o $< $@
+
+$(BUILD_DIR):
+ $(Q) mkdir -p $@
+
+test: | $(BUILD_DIR)
+ $(Q) cc -O2 -DPLATFORM_DEVICE=1 $(shell $(NWLINK) eadk-cflags-simulator) test/perft.c -o $(BUILD_DIR)/perft
+ $(Q) $(BUILD_DIR)/perft
+
+clean:
+ $(Q) rm -rf $(BUILD_DIR)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8787e9e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,178 @@
+
+
+
+
+NumChess
+
+
+ Chess for the NumWorks N0120 calculator.
+ 12 bots, endless puzzles and a two-player clock, in a native app of about 20 KB.
+
+
+
+
+
+
+
+
+
+ Download chess.nwa
+ · Install
+ · Controls
+ · Build
+
+
+
+
+
+
+## Highlights
+
+* **12 bots**, from Pip (150) to Atlas (2100), most of them at the beginner end.
+* **Endless puzzles** created on the calculator: mate in 1, 2 or 3, or find the one winning move. A rating and a streak track your progress.
+* **Two-player mode** with a chess clock. The board turns to face whoever is to move.
+* **Fast and small:** about 20 KB installed. The piece set takes 658 bytes, and there's no puzzle database.
+
+
+
+  Main menu |
+  A game against Nova |
+
+
+
+## Play the bots
+
+Pick an opponent and a color (White, random or Black), then play. Each bot beats the one below it in bot-vs-bot matches. The Elo numbers are rough estimates.
+
+
+
+
+
+
+
+  Choose a bot and your color |
+  Atlas, the strongest bot, thinks up to 2.5 s per move |
+
+
+  Legal moves as dots, captures as rings |
+  Check glow, last-move highlight, captured pieces with material count |
+
+
+  Promotion picker |
+  Result card with rematch |
+
+
+  Back opens the pause menu: undo, flip, resign |
+ |
+
+
+
+## Puzzles
+
+Every puzzle is generated on the calculator. It plays a quick game between a careful side and a blunder-prone side, then stops at a position with a forced mate or a single winning move. Puzzles never run out, and the app stores none of them.
+
+
+
+
+
+* **Modes:** Mix, Mate in 1, Mate in 2, Mate in 3 and Best move.
+* **Opening move:** each puzzle starts by playing the opponent's last move, which is the mistake you punish.
+* **Accepted solutions:** any move that still forces the mate counts. The opponent defends as stubbornly as possible.
+* **Rating:** it goes up or down after each puzzle, and Mix and Best move pick puzzles near it. A streak counts your solves in a row.
+* **Pause menu:** Hint highlights the piece to move, Solution plays the answer, and Next skips.
+* **Speed:** the next puzzle is prepared while you look at the solved one.
+
+
+
+  Puzzle modes |
+  Mate in 1: the pill shows who is to move |
+
+
+  Mate in 3 as Black: the board flips for you |
+  Best move: find the knight fork |
+
+
+  Hint |
+  Wrong move: it's taken back and your rating drops |
+
+
+  Solved |
+  Hint, solution, next |
+
+
+
+## Two players
+
+Play over the board with a chess clock. After every move the board turns to face the next player. If you prefer a fixed board, use Flip in the pause menu.
+
+
+
+
+
+
+
+  1+0 to 30+0, or no clock |
+  The active clock is lit |
+  Under 10 s it turns red and shows tenths |
+
+
+
+All the rules are included: castling, en passant, underpromotion, and draws by stalemate, threefold repetition, the 50-move rule or insufficient material. You can also agree to a draw or resign.
+
+## Controls
+
+| Key | Action |
+|---|---|
+| Arrows | Move the cursor (it wraps around the edges) |
+| OK or EXE | Select a piece, then play a move |
+| Back | Deselect, open the pause menu, or go back a screen |
+| ⌫ | Undo |
+| Home | Quit |
+
+## Install
+
+1. Download **[chess.nwa](https://github.com/Mason363/NumChess/releases/latest/download/chess.nwa)** from the [latest release](https://github.com/Mason363/NumChess/releases/latest).
+2. Plug the calculator into your computer and open **[my.numworks.com/apps](https://my.numworks.com/apps)** in Chrome or Edge.
+3. Upload `chess.nwa`. **Chess** appears at the end of the calculator's home screen.
+
+If you're building from source, `make run` builds and installs in one step.
+
+## Small by design
+
+
+
+
+
+| | |
+|---|---|
+| Installed size | about 20 KB (20,074 bytes of code and data) |
+| Piece set | 658 bytes: 3-bit anti-aliased sprites, and symmetric pieces store only half |
+| Puzzle data | none: every puzzle is generated live |
+| RAM | about 52 KB, all static, no heap |
+
+* **Engine** (`src/chess.c`):
+ * 0x88 board with incremental hashing and evaluation.
+ * PeSTO piece-square tables, mirrored and packed into 384 bytes.
+ * Alpha-beta search with PVS, a transposition table, null-move pruning, late-move reductions, killer moves and history heuristics.
+ * A mate solver.
+ * Bots get weaker through search depth, evaluation noise and occasional random moves.
+* **Graphics** (`src/main.c`):
+ * Each board square is drawn in one pass: highlights, check glow, move hints and the piece, all blended in RGB565.
+ * Rounded cards and circles are anti-aliased.
+ * Moves animate with an ease-out curve.
+ * Text uses the calculator's own fonts.
+* **Pieces** are drawn as signed-distance shapes by `tools/pieces.py` and rendered to sprites. `tools/icon.py` makes the app icon the same way.
+* **Build:** `-Os`, LTO, and small replacements for the libc routines.
+
+## Build
+
+Requirements: the ARM embedded toolchain (`arm-none-eabi-gcc`) and Node.js, which `make` uses to fetch NumWorks' `nwlink` SDK.
+
+```sh
+make # output/chess.nwa
+make check # link it the way the calculator does and print the installed size
+make test # engine tests on the host: perft on six positions, hashing, search benchmark
+make run # install on a connected calculator
+```
+
+Every push is built by GitHub Actions. Pushing a `v*` tag publishes a release with `chess.nwa` attached.
diff --git a/docs/bots.gif b/docs/bots.gif
new file mode 100644
index 0000000..d5bc49e
Binary files /dev/null and b/docs/bots.gif differ
diff --git a/docs/bots.png b/docs/bots.png
new file mode 100644
index 0000000..560115b
Binary files /dev/null and b/docs/bots.png differ
diff --git a/docs/check.png b/docs/check.png
new file mode 100644
index 0000000..ee73208
Binary files /dev/null and b/docs/check.png differ
diff --git a/docs/clock-low.png b/docs/clock-low.png
new file mode 100644
index 0000000..987a554
Binary files /dev/null and b/docs/clock-low.png differ
diff --git a/docs/clock.gif b/docs/clock.gif
new file mode 100644
index 0000000..5a8610d
Binary files /dev/null and b/docs/clock.gif differ
diff --git a/docs/clock.png b/docs/clock.png
new file mode 100644
index 0000000..ca5cb6b
Binary files /dev/null and b/docs/clock.png differ
diff --git a/docs/gameover.png b/docs/gameover.png
new file mode 100644
index 0000000..3e1253f
Binary files /dev/null and b/docs/gameover.png differ
diff --git a/docs/hero.gif b/docs/hero.gif
new file mode 100644
index 0000000..bc0070c
Binary files /dev/null and b/docs/hero.gif differ
diff --git a/docs/icon.png b/docs/icon.png
new file mode 100644
index 0000000..c4de384
Binary files /dev/null and b/docs/icon.png differ
diff --git a/docs/italian.png b/docs/italian.png
new file mode 100644
index 0000000..f72c7a2
Binary files /dev/null and b/docs/italian.png differ
diff --git a/docs/menu.png b/docs/menu.png
new file mode 100644
index 0000000..3c0b5b1
Binary files /dev/null and b/docs/menu.png differ
diff --git a/docs/pause.png b/docs/pause.png
new file mode 100644
index 0000000..a5021e1
Binary files /dev/null and b/docs/pause.png differ
diff --git a/docs/pieces.png b/docs/pieces.png
new file mode 100644
index 0000000..c97d26d
Binary files /dev/null and b/docs/pieces.png differ
diff --git a/docs/promotion.png b/docs/promotion.png
new file mode 100644
index 0000000..0d92c45
Binary files /dev/null and b/docs/promotion.png differ
diff --git a/docs/puzzle-best.png b/docs/puzzle-best.png
new file mode 100644
index 0000000..4bccdcf
Binary files /dev/null and b/docs/puzzle-best.png differ
diff --git a/docs/puzzle-hint.png b/docs/puzzle-hint.png
new file mode 100644
index 0000000..4499060
Binary files /dev/null and b/docs/puzzle-hint.png differ
diff --git a/docs/puzzle-mate1.png b/docs/puzzle-mate1.png
new file mode 100644
index 0000000..643135a
Binary files /dev/null and b/docs/puzzle-mate1.png differ
diff --git a/docs/puzzle-mate3.png b/docs/puzzle-mate3.png
new file mode 100644
index 0000000..be38ede
Binary files /dev/null and b/docs/puzzle-mate3.png differ
diff --git a/docs/puzzle-menu.png b/docs/puzzle-menu.png
new file mode 100644
index 0000000..e5592ac
Binary files /dev/null and b/docs/puzzle-menu.png differ
diff --git a/docs/puzzle-pause.png b/docs/puzzle-pause.png
new file mode 100644
index 0000000..00224c7
Binary files /dev/null and b/docs/puzzle-pause.png differ
diff --git a/docs/puzzle-solved.png b/docs/puzzle-solved.png
new file mode 100644
index 0000000..1fef695
Binary files /dev/null and b/docs/puzzle-solved.png differ
diff --git a/docs/puzzle-wrong.png b/docs/puzzle-wrong.png
new file mode 100644
index 0000000..9c75747
Binary files /dev/null and b/docs/puzzle-wrong.png differ
diff --git a/docs/puzzle.gif b/docs/puzzle.gif
new file mode 100644
index 0000000..f479694
Binary files /dev/null and b/docs/puzzle.gif differ
diff --git a/docs/roster.png b/docs/roster.png
new file mode 100644
index 0000000..806995a
Binary files /dev/null and b/docs/roster.png differ
diff --git a/docs/select.png b/docs/select.png
new file mode 100644
index 0000000..9ee0957
Binary files /dev/null and b/docs/select.png differ
diff --git a/docs/time.png b/docs/time.png
new file mode 100644
index 0000000..a6449da
Binary files /dev/null and b/docs/time.png differ
diff --git a/src/chess.c b/src/chess.c
new file mode 100644
index 0000000..8c28bfc
--- /dev/null
+++ b/src/chess.c
@@ -0,0 +1,736 @@
+#include "chess.h"
+#include
+#include
+
+Pos P;
+Undo H[MAXGAME + MAXPLY];
+int hp;
+uint32_t nodes;
+
+static uint64_t Z[16 * 64 + 25];
+#define ZP(pc, s) Z[(pc) * 64 + (s)]
+#define Zc (Z + 1024)
+#define Zep (Z + 1040)
+#define Zside Z[1048]
+
+/* PeSTO piece-square tables (Ronald Friederich), mirrored across the
+ * d/e files and halved to fit in int8. Rows run rank 8 -> rank 1. */
+static const int8_t PST[12][32] = {
+ {0,0,0,0,22,42,47,41,-6,8,21,24,-9,8,5,11,-13,2,0,7,-9,7,0,-2,-14,9,1,-9,0,0,0,0},
+ {0,0,0,0,91,85,73,70,45,46,35,31,12,10,4,1,3,3,-3,-3,-1,2,-3,0,2,3,2,6,0,0,0,0},
+ {-68,-26,-33,3,-22,-8,34,15,-1,33,42,37,3,9,22,23,-5,6,9,10,-10,4,7,7,-12,-17,2,-1,-32,-10,-21,-12},
+ {-39,-25,-10,-15,-19,-8,-12,-3,-16,-10,0,2,-9,3,8,11,-9,0,8,10,-11,-6,-1,6,-21,-11,-7,-2,-23,-25,-10,-9},
+ {-9,3,-31,-15,-18,9,10,4,-4,19,23,19,-1,3,14,22,0,6,6,15,3,8,11,7,1,12,9,2,-13,-10,-6,-8},
+ {-9,-9,-5,-4,-5,-2,-1,-4,2,-2,2,-1,0,3,6,6,-4,0,6,7,-7,-2,3,6,-10,-8,-4,1,-10,-3,-10,-3},
+ {19,18,10,29,18,15,31,36,3,20,18,13,-11,-5,11,13,-15,-5,-5,2,-19,-7,-4,-3,-29,-5,-2,-2,-11,-12,2,8},
+ {5,5,8,7,4,5,4,2,1,1,1,2,2,1,4,1,-2,-1,1,0,-5,-2,-4,-2,-2,-4,-2,-2,-7,2,-2,-1},
+ {4,11,18,18,8,-3,13,-4,11,8,16,9,-6,-7,0,-4,-3,-6,-3,-3,-2,4,-2,-2,-8,-3,7,3,-13,-12,-8,-1},
+ {3,8,10,14,-4,13,14,25,-3,6,11,24,10,20,16,26,1,17,13,20,-3,-4,8,4,-13,-15,-13,-8,-18,-12,-13,-12},
+ {-13,6,-4,-18,0,-10,-6,-4,-8,12,2,-9,-13,-8,-9,-14,-25,-8,-18,-21,-10,-7,-13,-22,2,4,-6,-27,0,15,-4,-11},
+ {-23,-8,-1,-7,0,10,13,9,6,15,17,9,-1,12,14,13,-7,1,11,13,-7,1,7,11,-11,-4,2,7,-24,-14,-9,-10},
+};
+static const int16_t VMG[7] = {0, 82, 337, 365, 477, 1025, 0};
+static const int16_t VEG[7] = {0, 94, 281, 297, 512, 936, 0};
+static const uint8_t PHASE[7] = {0, 0, 1, 1, 2, 4, 0};
+
+static const int8_t KN[8] = {33, 31, 18, 14, -33, -31, -18, -14};
+static const int8_t KG[8] = {1, -1, 16, -16, 17, 15, -17, -15};
+
+#define SQ64(s) (((s) + ((s) & 7)) >> 1)
+#define OFF(s) ((s) & 0x88)
+#define LAST(s) ((unsigned)((s) - 16) >= 96)
+
+static void upd(int sq, int pc, int sign) {
+ int t = TYPE(pc), f = sq & 7, r = sq >> 4;
+ if (f > 3) f = 7 - f;
+ if (!COLOR(pc)) r = 7 - r;
+ int i = r * 4 + f;
+ int mg = VMG[t] + 2 * PST[t * 2 - 2][i], eg = VEG[t] + 2 * PST[t * 2 - 1][i];
+ if (COLOR(pc) ^ (sign < 0)) mg = -mg, eg = -eg;
+ P.mg += mg;
+ P.eg += eg;
+ P.phase += sign * PHASE[t];
+ P.cnt[pc] += sign;
+ P.hash ^= ZP(pc, SQ64(sq));
+}
+
+static void put(int sq, int pc) {
+ P.b[sq] = pc;
+ upd(sq, pc, 1);
+ if (TYPE(pc) == KING) P.king[COLOR(pc)] = sq;
+}
+
+static void del(int sq) {
+ upd(sq, P.b[sq], -1);
+ P.b[sq] = 0;
+}
+
+static uint64_t rng64(void) {
+ static uint64_t s = 0x9E3779B97F4A7C15ull;
+ s ^= s >> 12, s ^= s << 25, s ^= s >> 27;
+ return s * 0x2545F4914F6CDD1Dull;
+}
+
+void ch_init(void) {
+ for (int i = 0; i < 16 * 64 + 25; i++) Z[i] = rng64();
+}
+
+void ch_reset(void) {
+ static const uint8_t back[8] = {ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK};
+ memset(&P, 0, sizeof P);
+ hp = 0;
+ for (int f = 0; f < 8; f++) {
+ put(f, back[f]);
+ put(16 + f, PAWN);
+ put(0x60 + f, BLACK | PAWN);
+ put(0x70 + f, BLACK | back[f]);
+ }
+ P.castle = 15;
+ P.ep = NOSQ;
+ P.hash ^= Zc[15];
+}
+
+static int attacked(int sq, int by) {
+ int c = by << 3;
+ for (int i = 15; i <= 17; i += 2) {
+ int s = by ? sq + i : sq - i;
+ if (!OFF(s) && P.b[s] == (c | PAWN)) return 1;
+ }
+ for (int i = 0; i < 8; i++) {
+ int s = sq + KN[i];
+ if (!OFF(s) && P.b[s] == (c | KNIGHT)) return 1;
+ s = sq + KG[i];
+ if (!OFF(s) && P.b[s] == (c | KING)) return 1;
+ for (; !OFF(s); s += KG[i]) {
+ int p = P.b[s];
+ if (p) {
+ if (COLOR(p) == by && (TYPE(p) == QUEEN || TYPE(p) == (i < 4 ? ROOK : BISHOP))) return 1;
+ break;
+ }
+ }
+ }
+ return 0;
+}
+
+int in_check(void) { return attacked(P.king[P.side], !P.side); }
+
+static int left_in_check(void) { return attacked(P.king[P.side ^ 1], P.side); }
+
+static Move *addp(Move *ml, int f, int t, int caps) {
+ if (!LAST(t)) *ml++ = MOVE(f, t, 0);
+ else for (int p = 3; p >= (caps ? 3 : 0); p--) *ml++ = MOVE(f, t, p);
+ return ml;
+}
+
+/* Pseudo-legal moves (castling is fully checked). caps: captures and queen promotions only. */
+static int gen(Move *ml0, int caps) {
+ Move *ml = ml0;
+ int us = P.side;
+ for (int sq = 0; sq < 120; sq++) {
+ int p = P.b[sq];
+ if (OFF(sq) || !p || COLOR(p) != us) continue;
+ int t = TYPE(p);
+ if (t == PAWN) {
+ int fwd = us ? -16 : 16, s = sq + fwd;
+ if (!P.b[s]) {
+ if (LAST(s) || !caps) ml = addp(ml, sq, s, caps);
+ if (!caps && ((sq >> 4) == (us ? 6 : 1)) && !P.b[s + fwd]) *ml++ = MOVE(sq, s + fwd, 0);
+ }
+ for (int d = -1; d <= 1; d += 2) {
+ int c = s + d;
+ if (!OFF(c) && ((P.b[c] && COLOR(P.b[c]) != us) || c == P.ep)) ml = addp(ml, sq, c, caps);
+ }
+ continue;
+ }
+ const int8_t *dir = t == KNIGHT ? KN : KG + (t == BISHOP) * 4;
+ int nd = (t == BISHOP || t == ROOK) ? 4 : 8, slide = t != KNIGHT && t != KING;
+ for (int i = 0; i < nd; i++) {
+ for (int s = sq + dir[i]; !OFF(s); s += dir[i]) {
+ int q = P.b[s];
+ if (q) {
+ if (COLOR(q) != us) *ml++ = MOVE(sq, s, 0);
+ break;
+ }
+ if (!caps) *ml++ = MOVE(sq, s, 0);
+ if (!slide) break;
+ }
+ }
+ }
+ if (!caps) {
+ int k = us ? 0x74 : 0x04;
+ if (P.king[us] == k && (P.castle & (us ? 12 : 3)) && !attacked(k, !us)) {
+ if ((P.castle & (us ? 4 : 1)) && !P.b[k + 1] && !P.b[k + 2] && !attacked(k + 1, !us))
+ *ml++ = MOVE(k, k + 2, 0);
+ if ((P.castle & (us ? 8 : 2)) && !P.b[k - 1] && !P.b[k - 2] && !P.b[k - 3] && !attacked(k - 1, !us))
+ *ml++ = MOVE(k, k - 2, 0);
+ }
+ }
+ return ml - ml0;
+}
+
+static int crmask(int s) {
+ static const uint8_t sq[6] = {0x00, 0x04, 0x07, 0x70, 0x74, 0x77}, mk[6] = {2, 3, 1, 8, 12, 4};
+ for (int i = 0; i < 6; i++)
+ if (s == sq[i]) return mk[i];
+ return 0;
+}
+
+void make(Move m) {
+ int f = MFROM(m), t = MTO(m), p = P.b[f], us = P.side;
+ Undo *u = &H[hp++];
+ u->hash = P.hash, u->m = m, u->pc = p, u->castle = P.castle, u->ep = P.ep, u->fifty = P.fifty;
+ u->cap = P.b[t];
+ P.fifty++;
+ if (TYPE(p) == PAWN) {
+ P.fifty = 0;
+ if (t == P.ep) {
+ int cs = t + (us ? 16 : -16);
+ u->cap = P.b[cs];
+ del(cs);
+ }
+ }
+ if (P.b[t]) del(t), P.fifty = 0;
+ del(f);
+ if (TYPE(p) == PAWN && LAST(t)) p = (us << 3) | (MPROMO(m) + KNIGHT);
+ put(t, p);
+ if (TYPE(p) == KING && (t - f == 2 || f - t == 2)) {
+ int rf = t > f ? f + 3 : f - 4;
+ del(rf);
+ put((f + t) >> 1, (us << 3) | ROOK);
+ }
+ P.hash ^= Zc[P.castle];
+ P.castle &= ~(crmask(f) | crmask(t));
+ P.hash ^= Zc[P.castle];
+ if (P.ep != NOSQ) P.hash ^= Zep[P.ep & 7];
+ P.ep = NOSQ;
+ if (TYPE(p) == PAWN && (t - f == 32 || f - t == 32)) P.ep = (f + t) >> 1, P.hash ^= Zep[P.ep & 7];
+ P.side ^= 1;
+ P.hash ^= Zside;
+}
+
+void unmake(void) {
+ Undo *u = &H[--hp];
+ int m = u->m, f = MFROM(m), t = MTO(m), us = P.side ^= 1;
+ if (m) {
+ del(t);
+ put(f, u->pc);
+ if (u->cap) put(TYPE(u->pc) == PAWN && t == u->ep ? t + (us ? 16 : -16) : t, u->cap);
+ if (TYPE(u->pc) == KING && (t - f == 2 || f - t == 2)) {
+ del((f + t) >> 1);
+ put(t > f ? f + 3 : f - 4, (us << 3) | ROOK);
+ }
+ }
+ P.castle = u->castle, P.ep = u->ep, P.fifty = u->fifty, P.hash = u->hash;
+}
+
+static void make_null(void) {
+ Undo *u = &H[hp++];
+ u->hash = P.hash, u->m = 0, u->castle = P.castle, u->ep = P.ep, u->fifty = P.fifty;
+ if (P.ep != NOSQ) P.hash ^= Zep[P.ep & 7];
+ P.ep = NOSQ;
+ P.side ^= 1;
+ P.hash ^= Zside;
+}
+
+int legal(Move *out) {
+ int n = gen(out, 0), k = 0;
+ for (int i = 0; i < n; i++) {
+ make(out[i]);
+ if (!left_in_check()) out[k++] = out[i];
+ unmake();
+ }
+ return k;
+}
+
+int is_legal(Move m) {
+ Move ml[256];
+ int n = legal(ml);
+ while (n--)
+ if (ml[n] == m) return 1;
+ return 0;
+}
+
+int gives_check(Move m) {
+ make(m);
+ int c = in_check();
+ unmake();
+ return c;
+}
+
+static int reps(void) {
+ int r = 0;
+ for (int i = hp - 2; i >= 0 && i >= hp - P.fifty; i -= 2) r += H[i].hash == P.hash;
+ return r;
+}
+
+static int insufficient(void) {
+ uint8_t *c = P.cnt;
+ if (c[PAWN] | c[ROOK] | c[QUEEN] | c[8 | PAWN] | c[8 | ROOK] | c[8 | QUEEN]) return 0;
+ return c[KNIGHT] + c[BISHOP] + c[8 | KNIGHT] + c[8 | BISHOP] <= 1;
+}
+
+int status(void) {
+ Move ml[256];
+ if (!legal(ml)) return in_check() ? CHECKMATE : STALEMATE;
+ if (P.fifty >= 100 || hp >= MAXGAME) return FIFTY;
+ if (reps() >= 2) return REPETITION;
+ if (insufficient()) return MATERIAL;
+ return ONGOING;
+}
+
+void san(Move m, char *o) {
+ int f = MFROM(m), t = MTO(m), p = P.b[f], ty = TYPE(p);
+ Move ml[256];
+ if (ty == KING && (t - f == 2 || f - t == 2)) {
+ const char *c = t > f ? "O-O" : "O-O-O";
+ while (*c) *o++ = *c++;
+ } else {
+ int cap = P.b[t] || (ty == PAWN && t == P.ep);
+ if (ty != PAWN) {
+ *o++ = " PNBRQK"[ty];
+ int n = legal(ml), amb = 0, sf = 0, sr = 0;
+ while (n--) {
+ int mf = MFROM(ml[n]);
+ if (MTO(ml[n]) == t && mf != f && P.b[mf] == p) {
+ amb = 1;
+ sf |= (mf & 7) == (f & 7);
+ sr |= (mf >> 4) == (f >> 4);
+ }
+ }
+ if (amb && (!sf || sr)) *o++ = 'a' + (f & 7);
+ if (amb && sf) *o++ = '1' + (f >> 4);
+ } else if (cap) {
+ *o++ = 'a' + (f & 7);
+ }
+ if (cap) *o++ = 'x';
+ *o++ = 'a' + (t & 7);
+ *o++ = '1' + (t >> 4);
+ if (ty == PAWN && LAST(t)) *o++ = '=', *o++ = "NBRQ"[MPROMO(m)];
+ }
+ make(m);
+ if (in_check()) *o++ = legal(ml) ? '+' : '#';
+ unmake();
+ *o = 0;
+}
+
+/* ---------------------------------------------------------------- Eval */
+
+int eval(void) {
+ int ph = P.phase > 24 ? 24 : P.phase;
+ int s = (P.mg * ph + P.eg * (24 - ph)) / 24;
+ uint8_t *c = P.cnt;
+ s += 30 * ((c[BISHOP] >= 2) - (c[8 | BISHOP] >= 2));
+ for (int w = 0; w < 2; w++) {
+ int sg = w ? -1 : 1, o = w << 3, x = 8 - o;
+ if (s * sg <= 0) continue;
+ if (!c[o | PAWN] && !c[o | ROOK] && !c[o | QUEEN] && c[o | KNIGHT] + c[o | BISHOP] <= 1) {
+ s /= 8;
+ } else if (!(c[x | PAWN] | c[x | KNIGHT] | c[x | BISHOP] | c[x | ROOK] | c[x | QUEEN])) {
+ /* Mop-up: drive the lone king to the edge and bring ours closer. */
+ int k = P.king[!w], K = P.king[w], kf = k & 7, kr = k >> 4;
+ int cd = (kf < 4 ? 3 - kf : kf - 4) + (kr < 4 ? 3 - kr : kr - 4);
+ int df = kf - (K & 7), dr = kr - (K >> 4);
+ s += sg * (300 + cd * 20 + (14 - (df < 0 ? -df : df) - (dr < 0 ? -dr : dr)) * 8);
+ }
+ }
+ return (P.side ? -s : s) + 8;
+}
+
+/* -------------------------------------------------------------- Search */
+
+#define TTN 2048
+typedef struct {
+ uint16_t key;
+ Move m;
+ int16_t s;
+ uint8_t d, f;
+} TTE;
+enum { T_EXACT = 1, T_LOWER, T_UPPER };
+static TTE tt[TTN];
+static Move killers[MAXPLY][2];
+static int16_t hist[16 * 64];
+#define MSN 3000
+static Move mstack[MSN];
+static int16_t mscore[MSN];
+static int msp;
+static uint64_t deadline;
+static int stopped;
+int think_depth, think_score;
+
+void ui_tick(void);
+
+static int tick(void) {
+ if ((++nodes & 1023) == 0) {
+ ui_tick();
+ if (eadk_timing_millis() > deadline) stopped = 1;
+ }
+ return stopped;
+}
+
+static void score_moves(Move *ml, int16_t *sc, int n, Move ttm, int ply) {
+ for (int i = 0; i < n; i++) {
+ Move m = ml[i];
+ int f = MFROM(m), t = MTO(m), p = P.b[f], v = P.b[t], s;
+ if (m == ttm) s = 30000;
+ else if (v || (TYPE(p) == PAWN && t == P.ep)) s = 20000 + TYPE(v ? v : PAWN) * 16 - TYPE(p);
+ else if (TYPE(p) == PAWN && LAST(t)) s = MPROMO(m) == 3 ? 19000 : -1000;
+ else if (m == killers[ply][0]) s = 18000;
+ else if (m == killers[ply][1]) s = 17990;
+ else s = hist[p * 64 + SQ64(t)];
+ sc[i] = s;
+ }
+}
+
+static Move pick(Move *ml, int16_t *sc, int n, int i) {
+ int b = i;
+ for (int j = i + 1; j < n; j++)
+ if (sc[j] > sc[b]) b = j;
+ Move m = ml[b];
+ int16_t s = sc[b];
+ ml[b] = ml[i], sc[b] = sc[i], ml[i] = m, sc[i] = s;
+ return m;
+}
+
+static int qs(int alpha, int beta, int ply) {
+ if (tick()) return 0;
+ int best = eval();
+ if (best >= beta || ply >= MAXPLY - 1 || msp > MSN - 256) return best;
+ if (best > alpha) alpha = best;
+ Move *ml = mstack + msp;
+ int16_t *sc = mscore + msp;
+ int n = gen(ml, 1);
+ msp += n;
+ score_moves(ml, sc, n, 0, ply);
+ for (int i = 0; i < n; i++) {
+ Move m = pick(ml, sc, n, i);
+ make(m);
+ if (left_in_check()) {
+ unmake();
+ continue;
+ }
+ int s = -qs(-beta, -alpha, ply + 1);
+ unmake();
+ if (s > best) {
+ best = s;
+ if (s > alpha) {
+ alpha = s;
+ if (s >= beta) break;
+ }
+ }
+ }
+ msp -= n;
+ return best;
+}
+
+int search(int depth, int alpha, int beta, int ply) {
+ if (ply && (P.fifty >= 100 || reps())) return 0;
+ int check = in_check();
+ if (check) depth++;
+ if (depth <= 0) return qs(alpha, beta, ply);
+ if (tick()) return 0;
+ if (ply >= MAXPLY - 1 || msp > MSN - 256) return eval();
+ if (alpha < -MATE + ply) alpha = -MATE + ply;
+ if (beta > MATE - ply - 1) beta = MATE - ply - 1;
+ if (alpha >= beta) return alpha;
+
+ TTE *e = &tt[P.hash & (TTN - 1)];
+ uint16_t key = P.hash >> 48;
+ Move ttm = 0;
+ if (e->key == key) {
+ ttm = e->m;
+ if (ply && e->d >= depth) {
+ int s = e->s;
+ if (s > MATE - 200) s -= ply;
+ else if (s < -MATE + 200) s += ply;
+ if (e->f == T_EXACT || (e->f == T_LOWER && s >= beta) || (e->f == T_UPPER && s <= alpha)) return s;
+ }
+ }
+ int pv = beta - alpha > 1, us = P.side << 3;
+ if (!pv && !check && depth >= 3 && ply &&
+ (P.cnt[us | KNIGHT] | P.cnt[us | BISHOP] | P.cnt[us | ROOK] | P.cnt[us | QUEEN]) && eval() >= beta) {
+ make_null();
+ int s = -search(depth - 3, -beta, -beta + 1, ply + 1);
+ unmake();
+ if (stopped) return 0;
+ if (s >= beta) return s > MATE - 200 ? beta : s;
+ }
+
+ Move *ml = mstack + msp;
+ int16_t *sc = mscore + msp;
+ int n = gen(ml, 0);
+ msp += n;
+ score_moves(ml, sc, n, ttm, ply);
+ int best = -INF, legalc = 0, a0 = alpha;
+ Move bm = 0;
+ for (int i = 0; i < n; i++) {
+ Move m = pick(ml, sc, n, i);
+ int p = P.b[MFROM(m)], t = MTO(m);
+ int quiet = !P.b[t] && !(TYPE(p) == PAWN && (t == P.ep || LAST(t)));
+ make(m);
+ if (left_in_check()) {
+ unmake();
+ continue;
+ }
+ legalc++;
+ int s, nd = depth - 1;
+ if (legalc == 1) {
+ s = -search(nd, -beta, -alpha, ply + 1);
+ } else {
+ int r = depth >= 3 && legalc > 3 && quiet && sc[i] < 17000 && !check && !in_check() ? 1 + (legalc > 8) : 0;
+ s = -search(nd - r, -alpha - 1, -alpha, ply + 1);
+ if (r && s > alpha) s = -search(nd, -alpha - 1, -alpha, ply + 1);
+ if (s > alpha && s < beta) s = -search(nd, -beta, -alpha, ply + 1);
+ }
+ unmake();
+ if (stopped) break;
+ if (s > best) {
+ best = s, bm = m;
+ if (s > alpha) {
+ alpha = s;
+ if (s >= beta) {
+ if (quiet) {
+ if (killers[ply][0] != m) killers[ply][1] = killers[ply][0], killers[ply][0] = m;
+ int16_t *h = &hist[p * 64 + SQ64(t)];
+ *h = *h + depth * depth > 16000 ? 16000 : *h + depth * depth;
+ }
+ break;
+ }
+ }
+ }
+ }
+ msp -= n;
+ if (stopped) return 0;
+ if (!legalc) return check ? -MATE + ply : 0;
+ int s = best;
+ if (s > MATE - 200) s += ply;
+ else if (s < -MATE + 200) s -= ply;
+ e->key = key, e->m = bm, e->s = s, e->d = depth;
+ e->f = best >= beta ? T_LOWER : best > a0 ? T_EXACT : T_UPPER;
+ return best;
+}
+
+static void limit(int ms) {
+ deadline = eadk_timing_millis() + ms;
+ stopped = 0;
+ msp = 0;
+}
+
+Move think(int maxd, int ms, int *score) {
+ Move root[256];
+ int n = legal(root), bs = 0;
+ if (!n) return 0;
+ limit(ms);
+ nodes = 0;
+ uint64_t t0 = eadk_timing_millis();
+ memset(killers, 0, sizeof killers);
+ for (int i = 0; i < 16 * 64; i++) hist[i] >>= 2;
+ think_depth = 0;
+ for (int d = 1; d <= maxd; d++) {
+ int alpha = -INF, cur = -INF;
+ for (int i = 0; i < n; i++) {
+ make(root[i]);
+ int s;
+ if (!i) {
+ s = -search(d - 1, -INF, INF, 1);
+ } else {
+ s = -search(d - 1, -alpha - 1, -alpha, 1);
+ if (s > alpha && !stopped) s = -search(d - 1, -INF, -alpha, 1);
+ }
+ unmake();
+ if (stopped) break;
+ if (s > alpha) {
+ Move m = root[i];
+ memmove(root + 1, root, i * sizeof(Move));
+ root[0] = m;
+ alpha = cur = s;
+ }
+ }
+ if (cur > -INF) bs = cur;
+ if (stopped) break;
+ think_depth = d;
+ if (MATE - (bs < 0 ? -bs : bs) <= d) break;
+ if ((eadk_timing_millis() - t0) * 2 > (uint64_t)ms) break;
+ }
+ if (score) *score = bs;
+ think_score = bs;
+ return root[0];
+}
+
+/* Score of one root move at a given depth, using a (alpha, beta) window. */
+int score_move(Move m, int depth, int alpha, int beta) {
+ make(m);
+ int s = -search(depth - 1, -beta, -alpha, 1);
+ unmake();
+ return s;
+}
+
+static int noise(int n) { return n ? (int)(eadk_random() % (n + 1) + eadk_random() % (n + 1)) - n : 0; }
+
+Move bot_move(int depth, int nz, int blunder, int ms) {
+ Move ml[256];
+ int n = legal(ml), bs;
+ if (n <= 1) return n ? ml[0] : 0;
+ if ((int)(eadk_random() % 100) < blunder) return ml[eadk_random() % n];
+ if (!nz && hp < 8) nz = 12, depth = depth < 4 ? depth : 4; /* opening variety */
+ Move best = think(depth, nz ? 60000 : ms, &bs);
+ if (!nz) return best;
+ int d = think_depth ? think_depth : 1, bv = -INF;
+ limit(60000);
+ for (int i = 0; i < n; i++) {
+ int s = ml[i] == best ? bs : score_move(ml[i], d, bs - 2 * nz, INF);
+ s += noise(nz);
+ if (s > bv) bv = s, best = ml[i];
+ }
+ return best;
+}
+
+/* ---------------------------------------------------------- Mate search */
+
+static int all_mated(int n, int co);
+static Move mate_first;
+
+/* Can the side to move force mate in at most n moves? co: attacker only checks. */
+int mate_in(int n, int co) {
+ Move *ml = mstack + msp;
+ int k = gen(ml, 0), found = 0;
+ msp += k;
+ for (int pass = 0; pass < 2 && !found; pass++) {
+ if (pass && (co || n == 1)) break;
+ for (int i = 0; i < k && !found; i++) {
+ make(ml[i]);
+ if (!left_in_check() && in_check() == !pass) found = all_mated(n - 1, co);
+ unmake();
+ if (found) mate_first = ml[i];
+ }
+ }
+ msp -= k;
+ return found;
+}
+
+/* Defender to move: does every reply lose to mate within n moves? */
+static int all_mated(int n, int co) {
+ Move *ml = mstack + msp;
+ int k = gen(ml, 0), any = 0, ok = 1;
+ msp += k;
+ for (int i = 0; i < k && ok; i++) {
+ make(ml[i]);
+ if (!left_in_check()) {
+ any = 1;
+ if (!n || !mate_in(n, co)) ok = 0;
+ }
+ unmake();
+ }
+ msp -= k;
+ return any ? ok : in_check();
+}
+
+int mated_within(int n) {
+ msp = 0;
+ return all_mated(n, 0);
+}
+
+int mate_len(int max, int co) {
+ msp = 0;
+ for (int n = 1; n <= max; n++)
+ if (mate_in(n, co)) return n;
+ return 0;
+}
+
+/* First move of a mate in at most n, or 0. */
+Move mate_move(int n) {
+ return mate_len(n, 1) || mate_len(n, 0) ? mate_first : 0;
+}
+
+/* Defender to move: the reply that postpones mate the longest. */
+Move defend(int n) {
+ Move ml[256];
+ int k = legal(ml), best = -1;
+ Move bm = ml[0];
+ for (int i = 0; i < k; i++) {
+ make(ml[i]);
+ int l = mate_len(n, 0);
+ unmake();
+ if (!l) l = 99;
+ if (l > best || (l == best && eadk_random() % 3 == 0)) best = l, bm = ml[i];
+ }
+ return bm;
+}
+
+/* ------------------------------------------------------------- Puzzles */
+
+Puzzle PZ;
+#define PMARGIN 180
+
+static int is_quiet(Move m) { return !P.b[MTO(m)] && !gives_check(m); }
+
+/* Is m the only good move here (every other move scores at least PMARGIN less)? */
+static int unique(Move b, int s) {
+ Move ml[256];
+ int n = legal(ml), d = think_depth;
+ for (int i = 0; i < n; i++)
+ if (ml[i] != b && score_move(ml[i], d, s - PMARGIN - 1, s - PMARGIN) > s - PMARGIN) return 0;
+ return 1;
+}
+
+static int tactic(void) {
+ int s, r, k = 0, made = 0;
+ Move ml[256], b = think(5, 60000, &s);
+ if (s < 200 || s > MATE - 200 || !unique(b, s)) return 0;
+ int df = 1;
+ while (df < 5 && think(df, 60000, &r) != b) df++;
+ PZ.sol[k++] = b;
+ make(b), made++;
+ while (k < 5 && legal(ml)) {
+ Move rep = think(4, 60000, &r);
+ make(rep), made++;
+ Move b2 = think(5, 60000, &r);
+ if (r < 200 || r > MATE - 200 || !unique(b2, r)) break;
+ PZ.sol[k++] = rep, PZ.sol[k++] = b2;
+ make(b2), made++;
+ }
+ while (made--) unmake();
+ PZ.nsol = k;
+ PZ.mate = 0;
+ PZ.rating = 450 + 150 * df + 120 * (k >> 1) + 150 * is_quiet(b) - 200 * (MTO(b) == MTO(H[hp - 1].m)) +
+ eadk_random() % 80;
+ return 1;
+}
+
+void puzzle_gen(int kind, int target) {
+ for (int tries = 0;;) {
+ ch_reset();
+ int solver = eadk_random() & 1, prev = 0, s;
+ for (int ply = 0; ply < 100; ply++) {
+ Move ml[256];
+ if (!legal(ml) || P.fifty > 30) break;
+ if (P.side == solver) {
+ if (ply >= 8) {
+ int n = mate_len(3, 1), ok = 0;
+ if (n && (kind == 0 || kind == n) && !(n == 3 && mate_len(2, 0))) {
+ PZ.sol[0] = mate_move(n);
+ PZ.nsol = 1, PZ.mate = n;
+ PZ.rating = 250 + n * 400 + 150 * is_quiet(PZ.sol[0]) + eadk_random() % 100;
+ ok = 1;
+ } else if (!n && (kind == 0 || kind == 4)) {
+ think(2, 60000, &s);
+ if (s >= 250 && s < MATE - 200 && s - prev >= 250) ok = tactic();
+ }
+ int dr = PZ.rating - target;
+ if (ok && !(kind & 3) && (dr < 0 ? -dr : dr) > 200 + 60 * tries++) ok = 0;
+ if (ok) {
+ PZ.pre = H[hp - 1].m;
+ unmake();
+ PZ.start = P;
+ return;
+ }
+ }
+ ui_tick();
+ make(bot_move(1, 40, 0, 60000));
+ } else {
+ Move m = eadk_random() % 8 ? bot_move(1, 110, 0, 60000) : (think(1, 60000, 0), ml[eadk_random() % legal(ml)]);
+ prev = -think_score;
+ make(m);
+ }
+ }
+ }
+}
diff --git a/src/chess.h b/src/chess.h
new file mode 100644
index 0000000..236e075
--- /dev/null
+++ b/src/chess.h
@@ -0,0 +1,80 @@
+#ifndef CHESS_H
+#define CHESS_H
+
+#include
+
+enum { EMPTY, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING };
+#define BLACK 8
+#define COLOR(p) ((p) >> 3)
+#define TYPE(p) ((p) & 7)
+#define NOSQ 0x80
+
+/* 0x88 squares: sq = rank * 16 + file */
+typedef uint16_t Move; /* from | to << 7 | promo << 14 (promo: 0 N, 1 B, 2 R, 3 Q) */
+#define MFROM(m) ((m) & 127)
+#define MTO(m) (((m) >> 7) & 127)
+#define MPROMO(m) ((m) >> 14)
+#define MOVE(f, t, p) ((Move)((f) | (t) << 7 | (p) << 14))
+
+#define MATE 30000
+#define INF 32000
+#define MAXPLY 64
+#define MAXGAME 600
+
+typedef struct {
+ uint8_t b[128];
+ uint8_t side, castle, ep, fifty;
+ uint8_t king[2];
+ uint8_t cnt[16];
+ int16_t mg, eg, phase;
+ uint64_t hash;
+} Pos;
+
+typedef struct {
+ uint64_t hash;
+ Move m;
+ uint8_t pc, cap, castle, ep, fifty;
+} Undo;
+
+extern Pos P;
+extern Undo H[];
+extern int hp;
+
+enum { ONGOING, CHECKMATE, STALEMATE, FIFTY, REPETITION, MATERIAL };
+
+void ch_init(void);
+void ch_reset(void);
+void make(Move m);
+void unmake(void);
+int in_check(void);
+int legal(Move *out);
+int is_legal(Move m);
+int gives_check(Move m);
+int status(void);
+void san(Move m, char *out);
+
+int eval(void);
+int search(int depth, int alpha, int beta, int ply);
+Move think(int depth, int ms, int *score);
+int score_move(Move m, int depth, int alpha, int beta);
+Move bot_move(int depth, int noise, int blunder, int ms);
+int mate_in(int n, int checks_only);
+int mate_len(int max, int checks_only);
+int mated_within(int n);
+Move mate_move(int n);
+Move defend(int n);
+
+typedef struct {
+ Pos start; /* position before the opponent's last move */
+ Move pre; /* that move, played when the puzzle starts */
+ Move sol[5]; /* solver moves and replies */
+ uint8_t nsol, mate;
+ int16_t rating;
+} Puzzle;
+extern Puzzle PZ;
+void puzzle_gen(int kind, int target);
+extern int think_depth;
+
+extern uint32_t nodes;
+
+#endif
diff --git a/src/icon.png b/src/icon.png
new file mode 100644
index 0000000..3c80b1a
Binary files /dev/null and b/src/icon.png differ
diff --git a/src/libc.c b/src/libc.c
new file mode 100644
index 0000000..e9ee8a9
--- /dev/null
+++ b/src/libc.c
@@ -0,0 +1,38 @@
+/* Minimal replacements for the few libc routines the app needs: much
+ * smaller than newlib's speed-optimized versions. */
+#include
+
+#define EXPORT __attribute__((used, externally_visible))
+
+EXPORT void *memcpy(void *d, const void *s, size_t n) {
+ char *o = d;
+ const char *i = s;
+ while (n--) *o++ = *i++;
+ return d;
+}
+
+EXPORT void *memmove(void *d, const void *s, size_t n) {
+ char *o = d;
+ const char *i = s;
+ if (o < i) return memcpy(d, s, n);
+ while (n--) o[n] = i[n];
+ return d;
+}
+
+EXPORT void *memset(void *d, int c, size_t n) {
+ char *o = d;
+ while (n--) *o++ = c;
+ return d;
+}
+
+EXPORT size_t strlen(const char *s) {
+ const char *e = s;
+ while (*e) e++;
+ return e - s;
+}
+
+EXPORT char *strcpy(char *d, const char *s) {
+ char *o = d;
+ while ((*o++ = *s++)) {}
+ return d;
+}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..36940a8
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,893 @@
+#include
+#include
+#include "chess.h"
+#include "sprites.h"
+
+const char eadk_app_name[] __attribute__((section(".rodata.eadk_app_name"))) = "Chess";
+const uint32_t eadk_api_level __attribute__((section(".rodata.eadk_api_level"))) = 0;
+
+typedef uint16_t C;
+#define RGB(r, g, b) ((C)((((r) & 0xF8) << 8) | (((g) & 0xFC) << 3) | ((b) >> 3)))
+
+#define BG RGB(0x26, 0x25, 0x22)
+#define CARD RGB(0x3A, 0x38, 0x35)
+#define GREEN RGB(0x81, 0xB6, 0x4C)
+#define RED RGB(0xE0, 0x43, 0x3B)
+#define WHITE 0xFFFF
+#define DIM RGB(0x98, 0x97, 0x95)
+#define INK RGB(0x26, 0x25, 0x22)
+#define SQ_L RGB(0xEB, 0xEC, 0xD0)
+#define SQ_D RGB(0x77, 0x95, 0x56)
+#define HL_L RGB(0xF5, 0xF6, 0x82)
+#define HL_D RGB(0xB9, 0xCA, 0x43)
+#define HINT RGB(0x5B, 0x9B, 0xE6)
+
+enum { K_LEFT, K_UP, K_DOWN, K_RIGHT, K_OK, K_BACK, K_UNDO = 17, K_EXE = 52 };
+enum { M_BOT, M_2P, M_PUZ };
+
+static C buf[900];
+
+/* ------------------------------------------------------------ Drawing */
+
+static C mix(C a, C b, int t) {
+ uint32_t A = (a | a << 16) & 0x07E0F81F, B = (b | b << 16) & 0x07E0F81F;
+ uint32_t M = ((A * (32 - t) + B * t) >> 5) & 0x07E0F81F;
+ return M | M >> 16;
+}
+
+static void fill(int x, int y, int w, int h, C c) {
+ eadk_display_push_rect_uniform((eadk_rect_t){x, y, w, h}, c);
+}
+
+static void text(const char *s, int x, int y, int big, C fg, C bg) {
+ eadk_display_draw_string(s, (eadk_point_t){x, y}, big, fg, bg);
+}
+
+static void ctext(const char *s, int cx, int y, int big, C fg, C bg) {
+ int n = 0;
+ for (const char *c = s; *c; c++) n += (*c & 0xC0) != 0x80;
+ text(s, cx - n * (big ? 10 : 7) / 2, y, big, fg, bg);
+}
+
+static char *itoa(int v, char *o) {
+ char t[8];
+ int n = 0;
+ if (v < 0) *o++ = '-', v = -v;
+ do t[n++] = '0' + v % 10; while (v /= 10);
+ while (n) *o++ = t[--n];
+ *o = 0;
+ return o;
+}
+
+static float clampf(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
+
+/* Anti-aliased rounded rectangle blended over what is on screen. */
+static void rrect(int x, int y, int w, int h, int r, C c) {
+ for (int j = 0; j < h; j++) {
+ int e = j < r || j >= h - r;
+ if (!e) {
+ fill(x, y + j, w, h - 2 * r, c);
+ j = h - r - 1;
+ continue;
+ }
+ eadk_rect_t R = {x, y + j, w, 1};
+ eadk_display_pull_rect(R, buf);
+ for (int i = 0; i < w; i++) {
+ float dx = i < r ? r - i - 0.5f : i >= w - r ? i + 0.5f - (w - r) : 0;
+ float dy = j < r ? r - j - 0.5f : j + 0.5f - (h - r);
+ float d = __builtin_sqrtf(dx * dx + dy * dy) - r;
+ buf[i] = mix(buf[i], c, (int)(32 * clampf(0.5f - d)));
+ }
+ eadk_display_push_rect(R, buf);
+ }
+}
+
+static void disc(int cx, int cy, int r, C c) { rrect(cx - r, cy - r, 2 * r, 2 * r, r, c); }
+
+/* Thick anti-aliased line blended over the screen. */
+static void stroke(float x0, float y0, float x1, float y1, float w, C c) {
+ int bx = (x0 < x1 ? x0 : x1) - w - 1, by = (y0 < y1 ? y0 : y1) - w - 1;
+ int bw = (x0 < x1 ? x1 - x0 : x0 - x1) + 2 * w + 3, bh = (y0 < y1 ? y1 - y0 : y0 - y1) + 2 * w + 3;
+ float ex = x1 - x0, ey = y1 - y0, l2 = ex * ex + ey * ey;
+ for (int j = 0; j < bh; j++) {
+ eadk_rect_t R = {bx, by + j, bw, 1};
+ eadk_display_pull_rect(R, buf);
+ for (int i = 0; i < bw; i++) {
+ float px = bx + i + 0.5f - x0, py = by + j + 0.5f - y0, t = clampf((px * ex + py * ey) / l2);
+ px -= ex * t, py -= ey * t;
+ buf[i] = mix(buf[i], c, (int)(32 * clampf(w / 2 + 0.5f - __builtin_sqrtf(px * px + py * py))));
+ }
+ eadk_display_push_rect(R, buf);
+ }
+}
+
+static void dim(int x, int y, int w, int h) {
+ for (int j = 0; j < h; j++) {
+ eadk_rect_t R = {x, y + j, w, 1};
+ eadk_display_pull_rect(R, buf);
+ for (int i = 0; i < w; i++) buf[i] = mix(buf[i], 0, 15);
+ eadk_display_push_rect(R, buf);
+ }
+}
+
+/* ------------------------------------------------------------ Sprites */
+
+static const C PCOL[2][3] = {
+ {RGB(0xF9, 0xF9, 0xF7), RGB(0x3A, 0x3A, 0x3A), RGB(0x3A, 0x3A, 0x3A)},
+ {RGB(0x52, 0x4F, 0x4D), RGB(0x16, 0x15, 0x14), RGB(0xA8, 0xA5, 0xA2)},
+};
+/* class -> (color a, color b, weight of b) with colors: 0 bg, 1 body, 2 ink, 3 detail */
+static const uint8_t SHADE[8][3] = {{0, 0, 0}, {1, 1, 0}, {2, 2, 0}, {0, 2, 19},
+ {0, 2, 10}, {3, 3, 0}, {1, 2, 16}, {1, 3, 16}};
+
+static int spx(int t, int x, int y) {
+ const uint8_t *m = &SPR[t - 1].x;
+ int w = m[2] & 0x7F;
+ y -= m[1];
+ if ((unsigned)y >= m[3]) return 0;
+ if (m[2] & 0x80 && x > 14) x = 29 - x;
+ x -= m[0];
+ if ((unsigned)x >= (unsigned)w) return 0;
+ unsigned i = (SPR[t - 1].off + y * w + x) * 3;
+ return ((SPRD[i >> 3] | SPRD[(i >> 3) + 1] << 8) >> (i & 7)) & 7;
+}
+
+static C shade(int pc, int x, int y, C bg) {
+ int k = spx(TYPE(pc), x, y);
+ if (!k) return bg;
+ const C *p = PCOL[COLOR(pc) > 1 ? x > 14 : COLOR(pc)];
+ C c[4] = {bg, p[0], p[1], p[2]};
+ return mix(c[SHADE[k][0]], c[SHADE[k][1]], SHADE[k][2]);
+}
+
+/* Draw a piece (30px, or 15px when small) over the screen content.
+ * COLOR 2 draws a half white / half black piece. */
+static void sprite(int pc, int X, int Y, int small) {
+ int s = small ? 15 : 30;
+ eadk_rect_t R = {X, Y, s, s};
+ eadk_display_pull_rect(R, buf);
+ for (int y = 0; y < s; y++)
+ for (int x = 0; x < s; x++) {
+ C *o = &buf[y * s + x];
+ if (small) {
+ C a = mix(shade(pc, 2 * x, 2 * y, *o), shade(pc, 2 * x + 1, 2 * y, *o), 16);
+ C b = mix(shade(pc, 2 * x, 2 * y + 1, *o), shade(pc, 2 * x + 1, 2 * y + 1, *o), 16);
+ *o = mix(a, b, 16);
+ } else {
+ *o = shade(pc, x, y, *o);
+ }
+ }
+ eadk_display_push_rect(R, buf);
+}
+
+/* -------------------------------------------------------------- Board */
+
+static int mode, flip, cur = 0x14, sel = -1, hintsq = -1, badsq = -1, ntg, incheck;
+static int anim_pc, anim_x, anim_y, anim_hide = -1;
+static Move tg[256];
+static char lastsan[10];
+
+static int sqx(int s) { return 30 * (flip ? 7 - (s & 7) : s & 7); }
+static int sqy(int s) { return 30 * (flip ? s >> 4 : 7 - (s >> 4)); }
+static int at(int vx, int vy) { return flip ? vy * 16 + 7 - vx : (7 - vy) * 16 + vx; }
+
+static void draw_sq(int s) {
+ int X = sqx(s), Y = sqy(s), light = ((s >> 4) + s) & 1, mark = 0;
+ Move lm = hp ? H[hp - 1].m : 0;
+ C base = light ? SQ_L : SQ_D;
+ if (s == sel || (hp && (s == MFROM(lm) || s == MTO(lm)))) base = light ? HL_L : HL_D;
+ if (s == hintsq) base = mix(base, HINT, 18);
+ if (s == badsq) base = mix(base, RED, 20);
+ int pc = s == anim_hide ? 0 : P.b[s];
+ if (sel >= 0)
+ for (int i = 0; i < ntg; i++)
+ if (MFROM(tg[i]) == sel && MTO(tg[i]) == s) mark = pc ? 2 : 1;
+ int chk = incheck && s == P.king[P.side];
+ for (int y = 0; y < 30; y++)
+ for (int x = 0; x < 30; x++) {
+ C c = base;
+ int dx = 2 * x - 29, dy = 2 * y - 29, d2 = dx * dx + dy * dy;
+ if (chk && d2 < 1300) c = mix(c, RED, d2 < 500 ? 30 : (1300 - d2) * 30 / 800);
+ if (mark == 2 && d2 > 600 && d2 < 900) c = mix(c, 0, 8);
+ if (pc) c = shade(pc, x, y, c);
+ if (mark == 1 && d2 < 150) c = mix(c, 0, d2 < 118 ? 8 : 4);
+ if (anim_pc) {
+ int u = X + x - anim_x, v = Y + y - anim_y;
+ if ((unsigned)u < 30 && (unsigned)v < 30) c = shade(anim_pc, u, v, c);
+ }
+ if (s == cur && sel != -2 && (x < 3 || x > 26 || y < 3 || y > 26)) c = light ? mix(c, INK, 13) : mix(c, WHITE, 22);
+ buf[y * 30 + x] = c;
+ }
+ eadk_display_push_rect((eadk_rect_t){X, Y, 30, 30}, buf);
+}
+
+static void draw_board(void) {
+ for (int s = 0; s < 120; s++)
+ if (!(s & 0x88)) draw_sq(s);
+}
+
+static void draw_rect_squares(int x, int y) {
+ for (int j = 0; j < 2; j++)
+ for (int i = 0; i < 2; i++) {
+ int vx = (x + i * 29) / 30, vy = (y + j * 29) / 30;
+ if (vx >= 0 && vx < 8 && vy >= 0 && vy < 8) draw_sq(at(vx, vy));
+ }
+}
+
+static void animate(Move m) {
+ int f = MFROM(m), t = MTO(m), x0 = sqx(f), y0 = sqy(f), x1 = sqx(t), y1 = sqy(t);
+ anim_pc = P.b[f], anim_hide = f, anim_x = x0, anim_y = y0;
+ int n = 7;
+ for (int i = 1; i <= n; i++) {
+ int ox = anim_x, oy = anim_y, k = n - i;
+ int e = 1024 - k * k * k * 1024 / (n * n * n); /* ease-out cubic */
+ anim_x = x0 + (x1 - x0) * e / 1024, anim_y = y0 + (y1 - y0) * e / 1024;
+ draw_rect_squares(ox, oy);
+ draw_rect_squares(anim_x, anim_y);
+ eadk_timing_msleep(14);
+ }
+ anim_pc = 0, anim_hide = -1;
+}
+
+/* ---------------------------------------------------------------- Bots */
+
+static const struct {
+ char name[6];
+ uint8_t pc, depth, noise, blunder;
+ uint16_t elo, ms;
+ C col;
+} BOTS[] = {
+ {"Pip", PAWN, 1, 250, 45, 150, 0, RGB(0x8B, 0xC3, 0x4A)},
+ {"Milo", PAWN, 1, 200, 28, 300, 0, RGB(0x4C, 0xAF, 0x50)},
+ {"Luna", KNIGHT, 1, 130, 14, 450, 0, RGB(0x26, 0xA6, 0x9A)},
+ {"Otto", KNIGHT, 2, 110, 10, 600, 0, RGB(0x29, 0xB6, 0xF6)},
+ {"Ivy", BISHOP, 2, 85, 7, 750, 0, RGB(0x42, 0x8D, 0xF5)},
+ {"Finn", BISHOP, 2, 65, 5, 900, 0, RGB(0x5C, 0x6B, 0xC0)},
+ {"Nova", ROOK, 3, 55, 3, 1050, 0, RGB(0x7E, 0x57, 0xC2)},
+ {"Hugo", ROOK, 4, 40, 2, 1200, 0, RGB(0xAB, 0x47, 0xBC)},
+ {"Vera", QUEEN, 4, 20, 1, 1400, 0, RGB(0xEC, 0x40, 0x7A)},
+ {"Rex", QUEEN, 5, 10, 0, 1600, 800, RGB(0xEF, 0x53, 0x50)},
+ {"Zara", KING, 7, 0, 0, 1850, 900, RGB(0xFF, 0x70, 0x43)},
+ {"Atlas", KING, 40, 0, 0, 2100, 2500, RGB(0xFF, 0xB3, 0x00)},
+};
+#define NBOTS 12
+
+static const struct { uint8_t min, inc; } TC[] = {{1, 0}, {3, 0}, {3, 2}, {5, 0}, {10, 0}, {15, 10}, {30, 0}, {0, 0}};
+
+static int bot = 4, pside = 0, pcol, tc = 3, autoflip = 1;
+static int32_t tm[2];
+static int prating = 800, pstreak, pdelta, spin_mode, spin_y, spin;
+
+/* -------------------------------------------------------------- Input */
+
+static int key(int ms) {
+ int32_t t = ms;
+ int e = eadk_event_get(&t);
+ return e == K_EXE ? K_OK : e;
+}
+
+static int arrow(int e, int *i, int n, int horiz) {
+ if (e == (horiz ? K_LEFT : K_UP) && *i > 0) return --*i, 1;
+ if (e == (horiz ? K_RIGHT : K_DOWN) && *i < n - 1) return ++*i, 1;
+ return 0;
+}
+
+/* ---------------------------------------------------------------- Menus */
+
+static void button(int x, int y, int w, int h, const char *s, int on, int icon) {
+ C c = on ? GREEN : CARD;
+ rrect(x, y, w, h, 8, c);
+ if (icon) {
+ int ty = y + (h - 40) / 2;
+ rrect(x + 8, ty, 40, 40, 9, SQ_L);
+ if (icon == KING) {
+ sprite(KING, x + 6, ty + 5, 0);
+ sprite(KING | BLACK, x + 20, ty + 5, 0);
+ } else {
+ sprite(icon, x + 13, ty + 5, 0);
+ }
+ text(s, x + 60, y + (h - 18) / 2, 1, WHITE, c);
+ } else {
+ ctext(s, x + w / 2, y + (h - 18) / 2, 1, on ? WHITE : RGB(0xDD, 0xDC, 0xDA), c);
+ }
+}
+
+/* Vertical list of buttons. Returns chosen index or -1. */
+static int list(const char *const *items, const uint8_t *icons, int n, int x, int w, int h, int gap, int *i) {
+ int y0 = (240 - n * h - (n - 1) * gap) / 2, redraw = 1;
+ for (;;) {
+ if (redraw)
+ for (int k = 0; k < n; k++) button(x, y0 + k * (h + gap), w, h, items[k], k == *i, icons ? icons[k] : 0);
+ int e = key(100000);
+ redraw = arrow(e, i, n, 0);
+ if (e == K_OK) return *i;
+ if (e == K_BACK) return -1;
+ }
+}
+
+static int main_menu(int *i) {
+ static const char *const items[] = {"Play", "Puzzles", "2 Players"};
+ static const uint8_t icons[] = {KNIGHT | BLACK, QUEEN | BLACK, KING};
+ fill(0, 0, 320, 240, BG);
+ return list(items, icons, 3, 60, 200, 56, 14, i);
+}
+
+static void avatar(int b, int cx, int cy, int r) {
+ disc(cx, cy, r, BOTS[b].col);
+ sprite(BOTS[b].pc, cx - 15, cy - 16, 0);
+}
+
+static int bot_select(void) {
+ fill(0, 0, 320, 240, BG);
+ int redraw = 1;
+ for (;;) {
+ if (redraw) {
+ for (int k = 0; k < NBOTS; k++) {
+ C c = k == bot ? CARD : BG;
+ rrect(6, k * 20, 148, 20, 6, c);
+ disc(20, k * 20 + 10, 5, BOTS[k].col);
+ text(BOTS[k].name, 32, k * 20 + 3, 0, k == bot ? WHITE : RGB(0xC8, 0xC7, 0xC5), c);
+ char e[6];
+ itoa(BOTS[k].elo, e);
+ text(e, 146 - 7 * strlen(e), k * 20 + 3, 0, DIM, c);
+ }
+ fill(160, 0, 160, 240, BG);
+ avatar(bot, 240, 62, 40);
+ ctext(BOTS[bot].name, 240, 114, 1, WHITE, BG);
+ char e[6];
+ itoa(BOTS[bot].elo, e);
+ ctext(e, 240, 136, 0, DIM, BG);
+ for (int k = 0; k < 3; k++) {
+ int x = 184 + k * 40;
+ rrect(x, 172, 32, 32, 8, k == pside ? GREEN : CARD);
+ sprite(KING | (k == 1 ? 16 : k == 2 ? BLACK : 0), x + 1, 173, 0);
+ }
+ }
+ int e = key(100000), o = bot, os = pside;
+ arrow(e, &bot, NBOTS, 0);
+ arrow(e, &pside, 3, 1);
+ redraw = o != bot || os != pside;
+ if (e == K_OK) return 1;
+ if (e == K_BACK) return 0;
+ }
+}
+
+static int time_select(void) {
+ fill(0, 0, 320, 240, BG);
+ int redraw = 1;
+ for (;;) {
+ if (redraw)
+ for (int k = 0; k < 8; k++) {
+ int x = 12 + (k & 3) * 76, y = 64 + (k >> 2) * 64;
+ char s[8], *o = s;
+ if (TC[k].min) {
+ o = itoa(TC[k].min, o);
+ *o++ = '+';
+ itoa(TC[k].inc, o);
+ } else {
+ strcpy(s, "\xE2\x88\x9E");
+ }
+ rrect(x, y, 68, 52, 8, k == tc ? GREEN : CARD);
+ ctext(s, x + 34, y + 17, 1, WHITE, k == tc ? GREEN : CARD);
+ }
+ int e = key(100000), o = tc;
+ if (e == K_LEFT && tc & 3) tc--;
+ if (e == K_RIGHT && (tc & 3) < 3) tc++;
+ if (e == K_UP && tc > 3) tc -= 4;
+ if (e == K_DOWN && tc < 4) tc += 4;
+ redraw = o != tc;
+ if (e == K_OK) return 1;
+ if (e == K_BACK) return 0;
+ }
+}
+
+/* Modal list drawn over the dimmed board. */
+static int overlay(const char *const *items, int n) {
+ dim(0, 0, 240, 240);
+ int h = n * 36 + 12, y = (240 - h) / 2, i = 0;
+ rrect(40, y, 160, h, 12, BG);
+ for (;;) {
+ for (int k = 0; k < n; k++) {
+ C c = k == i ? GREEN : BG;
+ rrect(46, y + 6 + k * 36, 148, 36, 8, c);
+ ctext(items[k], 120, y + 15 + k * 36, 1, WHITE, c);
+ }
+ int e = key(100000);
+ arrow(e, &i, n, 0);
+ if (e == K_OK || e == K_BACK) return e == K_OK ? i : -1;
+ }
+}
+
+/* --------------------------------------------------------------- Panel */
+
+static const uint8_t START[7] = {0, 8, 2, 2, 2, 1, 0};
+static const uint8_t WORTH[7] = {0, 1, 3, 3, 5, 9, 0};
+
+static int material(int c) {
+ int m = 0;
+ for (int t = PAWN; t < KING; t++) m += WORTH[t] * P.cnt[c << 3 | t];
+ return m;
+}
+
+/* Pieces captured by side c, drawn as small icons from (x, y). */
+static void captures(int c, int y) {
+ int x = 244;
+ for (int t = QUEEN; t >= PAWN; t--) {
+ int n = START[t] - P.cnt[(!c) << 3 | t];
+ for (int k = 0; k < n; k++, x += 7) {
+ if (x > 302) x = 244, y += 15;
+ sprite(t | (!c) << 3, x, y, 1);
+ }
+ if (n > 0) x += 6;
+ }
+ int d = material(c) - material(!c);
+ if (d > 0) {
+ char s[6] = "+";
+ itoa(d, s + 1);
+ text(s, x > 290 ? 244 : x + 4, x > 290 ? y + 15 : y, 0, DIM, BG);
+ }
+}
+
+static void fmt_clock(int32_t ms, char *o) {
+ if (ms < 0) ms = 0;
+ if (ms < 10000) {
+ o = itoa(ms / 1000, o);
+ *o++ = '.', *o++ = '0' + ms / 100 % 10, *o = 0;
+ } else {
+ int s = (ms + 999) / 1000;
+ o = itoa(s / 60, o);
+ *o++ = ':', *o++ = '0' + s % 60 / 10, *o++ = '0' + s % 10, *o = 0;
+ }
+}
+
+static void clock_pill(int c, int y) {
+ int on = P.side == c;
+ C bg = on ? (tm[c] < 10000 && TC[tc].min ? RED : WHITE) : CARD, fg = on ? (bg == RED ? WHITE : INK) : DIM;
+ rrect(244, y, 72, 34, 8, bg);
+ if (TC[tc].min) {
+ char s[8];
+ fmt_clock(tm[c], s);
+ ctext(s, 280, y + 8, 1, fg, bg);
+ } else {
+ sprite(KING | c << 3, 265, y + 2, 0);
+ }
+}
+
+static void feedback(int ok) {
+ fill(244, 84, 72, 76, BG);
+ if (ok < 0) return;
+ disc(280, 120, 26, ok ? GREEN : RED);
+ if (ok) {
+ stroke(268, 121, 276, 129, 5, WHITE);
+ stroke(276, 129, 292, 112, 5, WHITE);
+ } else {
+ stroke(270, 110, 290, 130, 5, WHITE);
+ stroke(290, 110, 270, 130, 5, WHITE);
+ }
+}
+
+static void puzzle_rating(void) {
+ char s[8];
+ fill(240, 176, 80, 64, BG);
+ itoa(prating, s);
+ ctext(s, 280, 186, 1, WHITE, BG);
+ if (pdelta) {
+ char *o = s;
+ if (pdelta > 0) *o++ = '+';
+ itoa(pdelta, o);
+ ctext(s, 280, 208, 0, pdelta > 0 ? GREEN : RED, BG);
+ }
+}
+
+static void panel(void) {
+ int top = !flip;
+ fill(240, 0, 80, 240, BG);
+ if (mode == M_PUZ) {
+ int s = !PZ.start.side;
+ C bg = s ? RGB(0x10, 0x10, 0x10) : WHITE;
+ rrect(244, 8, 72, 34, 8, bg);
+ char t[10] = "Mate in ";
+ t[8] = '0' + PZ.mate;
+ ctext(PZ.mate ? t : "Best move", 280, 18, 0, s ? WHITE : INK, bg);
+ if (pstreak > 1) {
+ char k[12];
+ strcpy(itoa(pstreak, k), " in a row");
+ ctext(k, 280, 52, 0, DIM, BG);
+ }
+ puzzle_rating();
+ return;
+ }
+ if (mode == M_BOT) {
+ avatar(bot, 280, 24, 18);
+ ctext(BOTS[bot].name, 280, 46, 1, WHITE, BG);
+ char e[6];
+ itoa(BOTS[bot].elo, e);
+ ctext(e, 280, 66, 0, DIM, BG);
+ captures(top, 88);
+ } else {
+ clock_pill(top, 6);
+ clock_pill(!top, 200);
+ captures(top, 46);
+ }
+ captures(!top, mode == M_BOT ? 200 : 164);
+ if (hp) {
+ char n[8];
+ strcpy(itoa((hp + 1) / 2, n), P.side ? "." : "...");
+ ctext(n, 280, 112 - (mode == M_2P) * 8, 0, DIM, BG);
+ ctext(lastsan, 280, 128 - (mode == M_2P) * 8, 1, WHITE, BG);
+ }
+}
+
+/* ------------------------------------------------------------ Moving */
+
+static void refresh(void) {
+ sel = -1;
+ ntg = legal(tg);
+ incheck = in_check();
+}
+
+static void set_lastsan(void) {
+ lastsan[0] = 0;
+ if (!hp) return;
+ Move m = H[hp - 1].m;
+ unmake();
+ san(m, lastsan);
+ make(m);
+}
+
+static void do_move(Move m) {
+ san(m, lastsan);
+ animate(m);
+ make(m);
+ refresh();
+ draw_board();
+ panel();
+}
+
+static int promo_pick(int to) {
+ static const uint8_t order[4] = {3, 0, 2, 1};
+ int X = sqx(to), Y = sqy(to), dir = Y ? -30 : 30, i = 0, c = P.side << 3;
+ for (;;) {
+ for (int k = 0; k < 4; k++) {
+ fill(X, Y + k * dir, 30, 30, k == i ? GREEN : WHITE);
+ sprite(c | (order[k] + KNIGHT), X, Y + k * dir, 0);
+ }
+ int e = key(100000);
+ if (e == K_UP || e == K_DOWN) i = (i + ((e == K_DOWN) == (dir > 0) ? 1 : 3)) & 3;
+ if (e == K_OK || e == K_BACK) {
+ draw_board();
+ return e == K_OK ? order[i] : -1;
+ }
+ }
+}
+
+/* Cursor handling. Returns a move, 0, or -1 when back is pressed with nothing selected. */
+static int human(int e) {
+ if (e <= K_RIGHT) {
+ int f = cur & 7, r = cur >> 4, d = flip ? -1 : 1, o = cur;
+ if (e == K_LEFT) f = (f - d) & 7;
+ if (e == K_RIGHT) f = (f + d) & 7;
+ if (e == K_UP) r = (r + d) & 7;
+ if (e == K_DOWN) r = (r - d) & 7;
+ cur = r * 16 + f;
+ draw_sq(o);
+ draw_sq(cur);
+ } else if (e == K_OK) {
+ if (sel >= 0)
+ for (int i = 0; i < ntg; i++)
+ if (MFROM(tg[i]) == sel && MTO(tg[i]) == cur) {
+ Move m = tg[i];
+ if (TYPE(P.b[sel]) == PAWN && (cur >> 4 == 0 || cur >> 4 == 7)) {
+ int p = promo_pick(cur);
+ if (p < 0) return 0;
+ m = MOVE(sel, cur, p);
+ }
+ return m;
+ }
+ int p = P.b[cur];
+ sel = p && COLOR(p) == P.side && sel != cur ? cur : -1;
+ draw_board();
+ } else if (e == K_BACK) {
+ if (sel < 0) return -1;
+ sel = -1;
+ draw_board();
+ } else if (e == K_UNDO) {
+ return -2;
+ }
+ return 0;
+}
+
+/* ---------------------------------------------------------------- Game */
+
+static int game_over(const char *title, const char *why) {
+ eadk_timing_msleep(400);
+ dim(0, 0, 240, 240);
+ rrect(20, 58, 200, 124, 12, BG);
+ ctext(title, 120, 74, 1, WHITE, BG);
+ ctext(why, 120, 98, 0, DIM, BG);
+ int i = 0;
+ for (;;) {
+ static const char *const b[2] = {"Rematch", "Menu"};
+ for (int k = 0; k < 2; k++) {
+ C c = k == i ? GREEN : CARD;
+ rrect(30 + k * 94, 130, 86, 38, 8, c);
+ ctext(b[k], 73 + k * 94, 140, 1, WHITE, c);
+ }
+ int e = key(100000);
+ arrow(e, &i, 2, 1);
+ if (e == K_OK) return !i;
+ if (e == K_BACK) return 0;
+ }
+}
+
+static void undo(int n) {
+ while (n-- && hp) unmake();
+ if (mode == M_2P && autoflip) flip = P.side;
+ set_lastsan();
+ refresh();
+ draw_board();
+ panel();
+}
+
+static void game(int md) {
+ static const char *const why[] = {"", "Checkmate", "Stalemate", "50-move rule", "Repetition", "Insufficient material"};
+again:
+ mode = md;
+ ch_reset();
+ refresh();
+ lastsan[0] = 0;
+ pcol = pside == 1 ? eadk_random() & 1 : pside >> 1;
+ flip = md == M_BOT && pcol;
+ int curs[2] = {0x14, 0x64};
+ cur = curs[flip];
+ tm[0] = tm[1] = TC[tc].min * 60000;
+ uint64_t last = eadk_timing_millis();
+ draw_board();
+ panel();
+ for (;;) {
+ const char *title = 0, *reason = 0;
+ int st = status(), winner = -1;
+ if (st) {
+ reason = why[st];
+ if (st == CHECKMATE) winner = !P.side;
+ }
+ if (md == M_2P && TC[tc].min && hp) {
+ uint64_t now = eadk_timing_millis();
+ int32_t before = tm[P.side];
+ tm[P.side] -= now - last;
+ last = now;
+ if (tm[P.side] <= 0) winner = !P.side, reason = "Timeout";
+ if (before / 100 != tm[P.side] / 100 && (tm[P.side] < 10000 || before / 1000 != tm[P.side] / 1000))
+ clock_pill(P.side, (P.side == !flip) ? 6 : 200);
+ } else {
+ last = eadk_timing_millis();
+ }
+ if (reason) {
+ if (md == M_BOT) title = winner < 0 ? "Draw" : winner == pcol ? "You won" : BOTS[bot].name;
+ else title = winner < 0 ? "Draw" : winner ? "Black won" : "White won";
+ static char t[12];
+ if (md == M_BOT && winner >= 0 && winner != pcol) strcpy(strcpy(t, title) + strlen(title), " won"), title = t;
+ if (game_over(title, reason)) goto again;
+ return;
+ }
+ if (md == M_BOT && P.side != pcol) {
+ uint64_t t0 = eadk_timing_millis();
+ spin_mode = 2, spin_y = 80;
+ Move m = bot_move(BOTS[bot].depth, BOTS[bot].noise, BOTS[bot].blunder, BOTS[bot].ms);
+ spin_mode = 0;
+ int32_t wait = 450 - (int32_t)(eadk_timing_millis() - t0);
+ if (wait > 0) eadk_timing_msleep(wait);
+ do_move(m);
+ continue;
+ }
+ int e = key(md == M_2P && TC[tc].min && hp ? 50 : 100000);
+ int m = human(e);
+ if (m == -2) {
+ undo(md == M_BOT ? (hp >= 2) * 2 : 1);
+ continue;
+ }
+ if (m == -1) {
+ static const char *const pb[] = {"Undo", "Flip", "Resign", "Menu"};
+ static const char *const p2[] = {"Undo", "Flip", "Draw", "Resign", "Menu"};
+ int k = md == M_BOT ? overlay(pb, 4) : overlay(p2, 5);
+ if (md == M_BOT && k >= 2) k++;
+ if (k == 0) {
+ undo(md == M_BOT ? (hp >= 2) * 2 : 1);
+ continue;
+ }
+ if (k == 1) {
+ flip ^= 1;
+ autoflip = flip == P.side;
+ }
+ if (k == 2 || k == 3) {
+ title = k == 2 ? "Draw" : P.side ? "White won" : "Black won";
+ if (md == M_BOT && k == 3) {
+ static char t[12];
+ strcpy(strcpy(t, BOTS[bot].name) + strlen(BOTS[bot].name), " won");
+ title = t;
+ }
+ if (game_over(title, k == 2 ? "Agreement" : "Resignation")) goto again;
+ return;
+ }
+ if (k == 4) return;
+ draw_board();
+ panel();
+ last = eadk_timing_millis();
+ continue;
+ }
+ if (m > 0) {
+ int mover = P.side;
+ curs[mover] = cur;
+ do_move(m);
+ if (md == M_2P) {
+ tm[mover] += TC[tc].inc * 1000;
+ if (autoflip && flip != P.side) {
+ eadk_timing_msleep(250);
+ flip = P.side;
+ cur = curs[P.side];
+ draw_board();
+ }
+ panel();
+ }
+ }
+ }
+}
+
+/* ------------------------------------------------------------- Puzzles */
+
+/* Progress animation while the engine works: 0 off, 1 ring (puzzles), 2 dots (bot). */
+void ui_tick(void) {
+ static uint64_t t;
+ uint64_t now = eadk_timing_millis();
+ if (!spin_mode || now - t < 70) return;
+ t = now;
+ spin++;
+ for (int k = 0; k < 8; k++) {
+ static const int8_t dx[8] = {0, 11, 16, 11, 0, -11, -16, -11}, dy[8] = {-16, -11, 0, 11, 16, 11, 0, -11};
+ if (spin_mode == 1) fill(278 + dx[(spin - k) & 7], 118 + dy[(spin - k) & 7], 5, 5, mix(BG, WHITE, 32 - k * 4));
+ else if (k < 3) fill(271 + k * 8, spin_y, 3, 3, (spin % 3) == k ? WHITE : CARD);
+ }
+}
+
+static void rate(int won) {
+ int e = 16 + (prating - PZ.rating) / 25;
+ e = e < 2 ? 2 : e > 30 ? 30 : e;
+ pdelta = won ? 32 - e : -e;
+ prating += pdelta;
+ if (prating < 100) prating = 100;
+ pstreak = won ? pstreak + 1 : 0;
+}
+
+/* Generate the next puzzle while the solved one stays on screen. */
+static Puzzle next;
+static int have_next;
+static void pregenerate(int kind) {
+ static Undo h[16];
+ Puzzle cur = PZ;
+ Pos p = P;
+ int n = hp;
+ memcpy(h, H, sizeof h);
+ spin_mode = 2, spin_y = 164;
+ puzzle_gen(kind, prating);
+ spin_mode = 0;
+ next = PZ, have_next = 1;
+ PZ = cur, P = p, hp = n;
+ memcpy(H, h, sizeof h);
+ fill(270, 164, 24, 3, BG);
+}
+
+static void puzzles(int kind) {
+ mode = M_PUZ;
+ have_next = 0;
+ for (;;) {
+ if (have_next) {
+ PZ = next, have_next = 0;
+ } else {
+ fill(240, 0, 80, 240, BG);
+ sel = -2;
+ draw_board();
+ dim(0, 0, 240, 240);
+ spin_mode = 1;
+ puzzle_gen(kind, prating);
+ spin_mode = 0;
+ }
+ P = PZ.start;
+ hp = 0;
+ flip = !P.side;
+ cur = flip ? 0x74 : 0x04;
+ refresh();
+ sel = -2;
+ lastsan[0] = 0;
+ pdelta = 0;
+ draw_board();
+ panel();
+ eadk_timing_msleep(500);
+ do_move(PZ.pre);
+ int step = 0, failed = 0, done = 0, rem = PZ.mate;
+ for (;;) {
+ int e = key(100000), m = 0;
+ if (done) {
+ if (e == K_OK) break;
+ if (e != K_BACK) continue;
+ m = -1;
+ } else {
+ m = human(e);
+ }
+ if (m == -1) {
+ static const char *const it[] = {"Hint", "Solution", "Next", "Menu"};
+ int k = overlay(it, 4);
+ draw_board();
+ if (k == 3) return;
+ if (k == 2) {
+ if (!done && !failed) rate(0);
+ break;
+ }
+ if (k < 0 || done) continue;
+ Move sm = PZ.mate ? (step ? mate_move(rem) : PZ.sol[0]) : PZ.sol[step];
+ if (k == 0) {
+ hintsq = MFROM(sm);
+ draw_sq(hintsq);
+ continue;
+ }
+ if (!failed) failed = 1, rate(0);
+ m = sm;
+ }
+ if (m <= 0) continue;
+ hintsq = -1;
+ make(m);
+ int ok = (!step && m == PZ.sol[0]) || status() == CHECKMATE ||
+ (PZ.mate ? mated_within(rem - 1) : m == PZ.sol[step]);
+ unmake();
+ if (!ok) {
+ do_move(m);
+ badsq = MTO(m);
+ draw_sq(badsq);
+ if (!failed) failed = 1, rate(0);
+ feedback(0);
+ puzzle_rating();
+ eadk_timing_msleep(700);
+ badsq = -1;
+ unmake();
+ set_lastsan();
+ refresh();
+ draw_board();
+ continue;
+ }
+ do_move(m);
+ step++;
+ if (status() == CHECKMATE || (!PZ.mate && step >= PZ.nsol)) {
+ if (!failed) rate(1);
+ done = 1;
+ sel = -2;
+ draw_board();
+ panel();
+ feedback(1);
+ pregenerate(kind);
+ continue;
+ }
+ feedback(1);
+ eadk_timing_msleep(350);
+ feedback(-1);
+ do_move(PZ.mate ? defend(--rem) : PZ.sol[step]);
+ step++;
+ }
+ hintsq = -1;
+ }
+}
+
+static int puzzle_menu(int *i) {
+ static const char *const items[] = {"Mix", "Mate in 1", "Mate in 2", "Mate in 3", "Best move"};
+ fill(0, 0, 320, 240, BG);
+ return list(items, 0, 5, 70, 180, 38, 8, i);
+}
+
+int main(void) {
+ ch_init();
+ int m = 0, pk = 0;
+ for (;;) {
+ if (main_menu(&m) < 0) return 0;
+ if (m == 0 && bot_select()) game(M_BOT);
+ if (m == 1 && puzzle_menu(&pk) >= 0) puzzles(pk);
+ if (m == 2 && time_select()) game(M_2P);
+ }
+}
diff --git a/src/sprites.h b/src/sprites.h
new file mode 100644
index 0000000..17bfbc7
--- /dev/null
+++ b/src/sprites.h
@@ -0,0 +1,44 @@
+/* Generated by tools/pieces.py: 3 bits per pixel, 8 pixels per 3 bytes. */
+static const struct { uint16_t off; uint8_t x, y, w, h; } SPR[6] = {
+ {0, 7, 4, 136, 23},
+ {184, 6, 3, 18, 24},
+ {616, 7, 2, 16, 25},
+ {1016, 6, 3, 137, 24},
+ {1232, 3, 3, 140, 24},
+ {1520, 6, 2, 137, 25},
+};
+static const uint8_t SPRD[658] = {
+ 0,0,108,0,64,201,0,32,39,0,168,36,0,230,36,0,230,36,0,168,
+ 36,0,32,37,0,0,73,0,0,73,0,64,217,0,32,39,0,48,37,0,
+ 64,37,0,32,39,0,168,36,0,230,36,0,101,219,156,36,73,178,109,219,
+ 74,146,36,178,109,219,220,182,109,0,0,0,36,0,0,0,0,128,163,6,
+ 0,0,0,0,160,36,17,0,0,0,0,230,196,18,1,0,0,48,37,73,
+ 44,0,0,0,234,77,146,104,0,0,80,105,147,36,209,0,0,114,146,36,
+ 73,34,0,152,146,36,73,146,136,0,148,36,73,146,36,30,48,53,137,228,
+ 36,73,4,168,143,180,41,73,18,17,178,52,96,74,146,68,4,53,0,152,
+ 147,36,17,1,0,192,148,36,73,52,0,0,168,36,73,18,15,0,64,37,
+ 73,146,68,3,0,204,73,146,36,17,1,0,114,146,36,73,68,0,156,36,
+ 73,146,36,13,144,36,73,146,36,73,2,148,36,73,146,36,137,8,217,182,
+ 109,219,182,45,130,219,182,109,219,182,13,0,0,96,3,0,0,0,0,72,
+ 18,0,0,0,0,40,17,0,0,0,0,200,22,0,0,0,0,80,34,0,
+ 0,0,0,76,26,0,0,0,64,201,150,8,0,0,40,39,137,69,0,0,
+ 228,36,105,44,0,0,149,36,109,34,2,192,156,228,77,226,1,128,156,164,
+ 79,98,1,128,146,188,73,18,1,192,156,36,73,226,1,192,148,36,73,162,
+ 1,0,148,36,73,34,0,0,166,36,73,52,0,0,32,217,182,4,0,0,
+ 38,73,146,52,0,0,38,219,182,53,0,192,36,73,146,164,1,156,156,36,
+ 73,98,141,146,147,36,73,18,75,178,109,219,182,109,91,219,182,109,219,182,
+ 109,0,73,128,4,38,17,19,48,139,152,129,89,155,12,204,177,29,96,178,
+ 109,3,156,36,9,0,230,36,0,48,39,1,128,57,9,0,204,73,0,96,
+ 78,2,0,115,18,0,152,147,0,192,148,4,0,166,36,0,56,73,2,40,
+ 73,18,64,37,73,192,73,146,4,73,146,36,42,73,146,80,182,109,27,219,
+ 182,109,0,0,0,0,8,0,0,9,76,0,0,74,131,2,0,152,35,76,
+ 152,134,73,2,70,69,3,7,128,84,52,96,0,136,105,0,54,96,0,9,
+ 96,2,6,64,3,166,97,0,164,97,18,5,96,210,38,75,0,168,88,78,
+ 2,0,74,146,36,0,32,39,73,2,0,115,146,36,0,64,253,255,15,0,
+ 80,219,182,0,0,37,73,2,0,144,36,73,0,48,73,146,4,0,114,146,
+ 36,0,32,219,182,13,0,220,182,109,0,0,0,3,0,0,16,0,0,155,
+ 0,0,148,4,0,0,41,0,0,0,1,0,0,8,0,0,64,192,36,73,
+ 194,228,88,254,148,36,201,229,36,73,174,36,73,114,57,73,146,79,73,146,
+ 28,114,146,228,160,146,36,1,166,36,9,64,181,109,129,201,73,2,76,146,
+ 36,76,146,36,65,78,146,4,178,109,219,224,182,109,3,0,0,0,
+};
diff --git a/test/perft.c b/test/perft.c
new file mode 100644
index 0000000..ba5b534
--- /dev/null
+++ b/test/perft.c
@@ -0,0 +1,106 @@
+/* Host-side engine tests: perft node counts on standard positions,
+ * incremental hash/eval consistency, and a quick search benchmark. */
+#include "../src/chess.c"
+#include
+#include
+#include
+
+uint64_t eadk_timing_millis() {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec * 1000ull + ts.tv_nsec / 1000000;
+}
+uint32_t eadk_random() { return (uint32_t)rand(); }
+void ui_tick(void) {}
+
+static void set_fen(const char *f) {
+ memset(&P, 0, sizeof P);
+ hp = 0;
+ for (int r = 7, c = 0; *f && *f != ' '; f++) {
+ if (*f == '/') r--, c = 0;
+ else if (*f >= '1' && *f <= '8') c += *f - '0';
+ else {
+ const char *pcs = " PNBRQK pnbrqk", *q = strchr(pcs, *f);
+ put(r * 16 + c++, q - pcs);
+ }
+ }
+ f++;
+ P.side = *f == 'b';
+ f += 2;
+ for (; *f && *f != ' '; f++) P.castle |= *f == 'K' ? 1 : *f == 'Q' ? 2 : *f == 'k' ? 4 : *f == 'q' ? 8 : 0;
+ f++;
+ P.ep = *f == '-' ? NOSQ : (f[1] - '1') * 16 + f[0] - 'a';
+ P.hash ^= Zc[P.castle];
+ if (P.ep != NOSQ) P.hash ^= Zep[P.ep & 7];
+ if (P.side) P.hash ^= Zside;
+}
+
+static int consistent(void) {
+ Pos s = P;
+ uint8_t b[128];
+ memcpy(b, P.b, 128);
+ memset(&P, 0, sizeof P);
+ for (int i = 0; i < 128; i++)
+ if (!OFF(i) && b[i]) put(i, b[i]);
+ P.side = s.side, P.castle = s.castle, P.ep = s.ep, P.fifty = s.fifty;
+ P.hash ^= Zc[P.castle];
+ if (P.ep != NOSQ) P.hash ^= Zep[P.ep & 7];
+ if (P.side) P.hash ^= Zside;
+ int ok = P.hash == s.hash && P.mg == s.mg && P.eg == s.eg && P.phase == s.phase &&
+ !memcmp(P.cnt, s.cnt, 16) && P.king[0] == s.king[0] && P.king[1] == s.king[1];
+ P = s;
+ return ok;
+}
+
+static uint64_t perft(int d) {
+ Move ml[256];
+ int n = legal(ml);
+ if (d == 1) return n;
+ uint64_t c = 0;
+ for (int i = 0; i < n; i++) {
+ make(ml[i]);
+ if (d == 2 && !consistent()) {
+ printf("inconsistent incremental state\n");
+ exit(1);
+ }
+ c += perft(d - 1);
+ unmake();
+ }
+ return c;
+}
+
+int main(void) {
+ static const struct { const char *fen; int d; uint64_t n; } T[] = {
+ {"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -", 5, 4865609},
+ {"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -", 4, 4085603},
+ {"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -", 5, 674624},
+ {"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq -", 4, 422333},
+ {"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ -", 4, 2103487},
+ {"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - -", 4, 3894594},
+ };
+ ch_init();
+ int fail = 0;
+ for (unsigned i = 0; i < sizeof T / sizeof *T; i++) {
+ set_fen(T[i].fen);
+ uint64_t n = perft(T[i].d);
+ printf("perft %u: %llu %s\n", i, (unsigned long long)n, n == T[i].n ? "ok" : "FAIL");
+ fail |= n != T[i].n;
+ }
+ static const char *bench[] = {
+ "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq -",
+ "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -",
+ "6k1/5ppp/8/8/8/8/5PPP/3R2K1 w - -",
+ };
+ for (unsigned i = 0; i < 3; i++) {
+ set_fen(bench[i]);
+ uint64_t t = eadk_timing_millis();
+ int s;
+ Move m = think(7, 100000, &s);
+ t = eadk_timing_millis() - t;
+ char buf[16];
+ san(m, buf);
+ printf("bench %u: %s score %d depth %d nodes %u in %llums\n", i, buf, s, think_depth, nodes,
+ (unsigned long long)t);
+ }
+ return fail;
+}
diff --git a/tools/icon.py b/tools/icon.py
new file mode 100644
index 0000000..6b9a53c
--- /dev/null
+++ b/tools/icon.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Render the home screen icon (src/icon.png, 55x56): a knight on a green tile."""
+import struct, sys, zlib, os
+sys.path.insert(0, os.path.dirname(__file__))
+import pieces
+
+W, H, SS = 55, 56, 6
+GREEN, BODY, INK = (0x81, 0xB6, 0x4C), (0xF9, 0xF9, 0xF7), (0x3A, 0x3A, 0x3A)
+tile = pieces.box(0, 0, W, H, 10)
+body, details, eye = pieces.knight()
+scale, ox, oy = 1.6, 3.5, 3.5 # icon px -> piece space
+
+
+def px(x, y):
+ acc = [0, 0, 0]
+ for sy in range(SS):
+ for sx in range(SS):
+ X, Y = x + (sx + 0.5) / SS, y + (sy + 0.5) / SS
+ c = (255, 255, 255)
+ if tile(X, Y) < 0:
+ c = GREEN
+ u, v = (X - ox) / scale + 0.8, (Y - oy) / scale
+ d = body(u, v)
+ if d < 0:
+ ink = d > -1.0 or eye(u, v) < 0 or any(g(u, v) < 0.5 for g in details)
+ c = INK if ink else BODY
+ acc = [a + b for a, b in zip(acc, c)]
+ return tuple(round(a / SS / SS) for a in acc)
+
+
+raw = b''.join(b'\0' + bytes(v for x in range(W) for v in px(x, y)) for y in range(H))
+
+
+def chunk(t, d):
+ return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d))
+
+
+open('src/icon.png', 'wb').write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0)) +
+ chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b''))
diff --git a/tools/pieces.py b/tools/pieces.py
new file mode 100644
index 0000000..e077d40
--- /dev/null
+++ b/tools/pieces.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+"""Rasterize the chess piece set into compact sprites (src/sprites.h).
+
+Pieces are described as signed distance fields and rendered with 8x8
+supersampling into a 30x30 cell. Each pixel is quantized to one of eight
+classes (3 bits):
+ 0 transparent 1 body 2 outline 3 outline 60% over background
+ 4 outline 30% 5 inner detail 6 outline/body 7 detail/body
+Symmetric pieces only store their left half.
+
+ python3 tools/pieces.py # writes src/sprites.h
+ python3 tools/pieces.py prev.png # also writes a zoomed preview (docs/pieces.png)
+"""
+import math, struct, sys, zlib
+
+N = 30 # cell size
+SS = 8 # supersampling per axis
+OUTLINE = 1.2 # outline width in pixels
+DETAIL = 0.55 # half width of inner detail strokes
+
+
+def length(x, y):
+ return math.sqrt(x * x + y * y)
+
+
+def circle(cx, cy, r):
+ return lambda x, y: length(x - cx, y - cy) - r
+
+
+def ellipse(cx, cy, rx, ry):
+ m = min(rx, ry)
+ return lambda x, y: (length((x - cx) / rx, (y - cy) / ry) - 1) * m
+
+
+def box(x0, y0, x1, y1, r=0.0):
+ cx, cy, hx, hy = (x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0) / 2, (y1 - y0) / 2
+
+ def f(x, y):
+ qx, qy = abs(x - cx) - hx + r, abs(y - cy) - hy + r
+ return length(max(qx, 0), max(qy, 0)) + min(max(qx, qy), 0) - r
+ return f
+
+
+def poly(pts):
+ def f(x, y):
+ d = 1e9
+ s = 1
+ n = len(pts)
+ for i in range(n):
+ ax, ay = pts[i]
+ bx, by = pts[i - 1]
+ ex, ey = bx - ax, by - ay
+ wx, wy = x - ax, y - ay
+ t = max(0, min(1, (wx * ex + wy * ey) / (ex * ex + ey * ey)))
+ d = min(d, length(wx - ex * t, wy - ey * t))
+ c1, c2, c3 = y >= ay, y < by, ex * wy > ey * wx
+ if (c1 and c2 and c3) or (not c1 and not c2 and not c3):
+ s = -s
+ return s * d
+ return f
+
+
+def seg(ax, ay, bx, by):
+ def f(x, y):
+ ex, ey, wx, wy = bx - ax, by - ay, x - ax, y - ay
+ t = max(0, min(1, (wx * ex + wy * ey) / (ex * ex + ey * ey)))
+ return length(wx - ex * t, wy - ey * t)
+ return f
+
+
+def union(*fs):
+ return lambda x, y: min(f(x, y) for f in fs)
+
+
+def smooth_union(k, *fs):
+ def f(x, y):
+ d = fs[0](x, y)
+ for g in fs[1:]:
+ e = g(x, y)
+ h = max(0, min(1, 0.5 + 0.5 * (e - d) / k))
+ d = e * (1 - h) + d * h - k * h * (1 - h)
+ return d
+ return f
+
+
+def sub(a, b):
+ return lambda x, y: max(a(x, y), -b(x, y))
+
+
+def mirror(pts):
+ """Close a left-half outline (top to bottom) into a symmetric polygon."""
+ return pts + [(N - x, y) for x, y in reversed(pts)]
+
+
+# ---------------------------------------------------------------- pieces
+def pawn():
+ body = smooth_union(1.2,
+ circle(15, 8.9, 4.7),
+ poly(mirror([(12.4, 13.2), (10.6, 16.2), (11.9, 16.6), (9.4, 22.8)])),
+ box(7.0, 22.2, 23.0, 26.8, 1.6))
+ return body, []
+
+
+def rook():
+ top = sub(box(8.2, 3.6, 21.8, 9.6, 0.6),
+ union(box(11.4, 2, 13.4, 6.4), box(16.6, 2, 18.6, 6.4)))
+ body = union(top,
+ poly(mirror([(10.2, 9), (10.6, 20.5)])),
+ box(8.6, 19.8, 21.4, 23.2, 0.6),
+ box(6.8, 22.6, 23.2, 26.8, 1.2))
+ details = [seg(9.5, 9.6, 20.5, 9.6), seg(9.8, 20.2, 20.2, 20.2), seg(8, 23.2, 22, 23.2)]
+ return body, details
+
+
+def bishop():
+ mitre = smooth_union(0.8,
+ ellipse(15, 14.2, 5.9, 6.9),
+ poly([(15, 6.0), (19.9, 12), (10.1, 12)]))
+ body = union(
+ circle(15, 4.6, 2.2),
+ mitre,
+ box(10.4, 19.6, 19.6, 22.6, 0.8),
+ poly(mirror([(9.5, 22), (7.4, 26.8)])),
+ box(7, 23.5, 23, 26.8, 1.2))
+ details = [seg(16.6, 10.6, 14.2, 14.4), seg(11.4, 19.7, 18.6, 19.7), seg(9.6, 22.8, 20.4, 22.8)]
+ return body, details
+
+
+def queen():
+ tips = [(5.6, 9.0), (10.2, 6.6), (15, 5.6), (19.8, 6.6), (24.4, 9.0)]
+ crown = poly([(8.4, 22.6), (5.4, 10.6), (10.6, 15.0), (10.0, 8.2), (13.9, 14.2), (15, 7.2),
+ (16.1, 14.2), (20.0, 8.2), (19.4, 15.0), (24.6, 10.6), (21.6, 22.6)])
+ body = union(crown, *[circle(x, y, 2.0) for x, y in tips],
+ box(8.0, 20.0, 22.0, 23.4, 0.8),
+ box(7, 23, 23, 26.8, 1.2))
+ details = [seg(9.0, 20.2, 21.0, 20.2), seg(8.6, 23.3, 21.4, 23.3)]
+ return body, details
+
+
+def king():
+ lobes = smooth_union(1.4,
+ circle(10.6, 14.6, 4.6),
+ circle(19.4, 14.6, 4.6),
+ ellipse(15, 13.6, 3.2, 4.2),
+ poly(mirror([(7.2, 15.5), (9.4, 22.6)])))
+ cross = union(box(14, 2.4, 16, 10.2, 0.4), box(11.8, 4.2, 18.2, 6.2, 0.4))
+ body = union(lobes, cross,
+ box(8.4, 20.4, 21.6, 23.4, 0.8),
+ box(7, 23, 23, 26.8, 1.2))
+ details = [seg(15, 10.4, 15, 17.5), seg(9.4, 20.4, 20.6, 20.4), seg(8.6, 23.3, 21.4, 23.3)]
+ return body, details
+
+
+def knight():
+ head = poly([
+ (22.2, 23.2), (22.6, 18.5), (22.2, 13.8), (20.8, 9.8), (18.4, 6.6), (15.8, 5.2),
+ (15.2, 2.8), (13.4, 4.8), (11.8, 3.6), (11.4, 6.4), (8.6, 9.6), (6.2, 13.6),
+ (5.8, 15.6), (7.2, 17.0), (9.4, 16.2), (11.2, 15.2), (13.6, 14.2),
+ (13.4, 16.4), (10.6, 19.6), (9.6, 23.2)])
+ body = smooth_union(0.8, head, box(7, 22.6, 23.2, 26.8, 1.2))
+ details = [seg(8.8, 23.2, 21.6, 23.2), seg(7.4, 14.4, 8.4, 13.8)]
+ eye = circle(12.2, 9.2, 1.0)
+ return body, details, eye
+
+
+def render(shape):
+ body, details = shape[0], shape[1]
+ eye = shape[2] if len(shape) > 2 else None
+ cls = []
+ for py in range(N):
+ row = []
+ for px in range(N):
+ inside = ink = det = 0
+ for sy in range(SS):
+ for sx in range(SS):
+ x = px + (sx + 0.5) / SS
+ y = py + (sy + 0.5) / SS
+ d = body(x, y)
+ if d < 0:
+ inside += 1
+ if d > -OUTLINE:
+ ink += 1
+ elif any(g(x, y) < DETAIL for g in details) or (eye and eye(x, y) < 0):
+ det += 1
+ a = inside / (SS * SS)
+ if a < 0.2:
+ c = 0
+ elif a < 0.5:
+ c = 4
+ elif a < 0.85:
+ c = 3
+ else:
+ fi, fd = ink / inside, det / inside
+ c = 2 if fi > 0.65 else 6 if fi > 0.3 else 5 if fd > 0.55 else 7 if fd > 0.2 else 1
+ row.append(c)
+ cls.append(row)
+ return cls
+
+
+PIECES = [('pawn', pawn), ('knight', knight), ('bishop', bishop),
+ ('rook', rook), ('queen', queen), ('king', king)]
+
+
+def bounds(img):
+ ys = [y for y in range(N) if any(img[y])]
+ xs = [x for x in range(N) if any(img[y][x] for y in range(N))]
+ return min(xs), min(ys), max(xs), max(ys)
+
+
+def symmetric(img):
+ return all(img[y][x] == img[y][N - 1 - x] for y in range(N) for x in range(N))
+
+
+def main():
+ imgs = [(name, render(f())) for name, f in PIECES]
+ out, bits, meta, off = [], [], [], 0
+ for name, img in imgs:
+ x0, y0, x1, y1 = bounds(img)
+ sym = symmetric(img)
+ if sym:
+ x0 = min(x0, N - 1 - x1)
+ w = N // 2 - x0
+ else:
+ w = x1 - x0 + 1
+ h = y1 - y0 + 1
+ px = [img[y][x] for y in range(y0, y1 + 1) for x in range(x0, x0 + w)]
+ bits.extend(px)
+ meta.append((off, x0, y0, w, h, sym))
+ off += len(px)
+ for i in range(0, len(bits), 8):
+ v = 0
+ for j, c in enumerate(bits[i:i + 8]):
+ v |= c << (3 * j)
+ out += [v & 255, (v >> 8) & 255, v >> 16]
+ out.append(0) # padding: the decoder reads two bytes at a time
+ with open('src/sprites.h', 'w') as f:
+ f.write('/* Generated by tools/pieces.py: 3 bits per pixel, 8 pixels per 3 bytes. */\n')
+ f.write('static const struct { uint16_t off; uint8_t x, y, w, h; } SPR[6] = {\n')
+ for off, x0, y0, w, h, sym in meta:
+ f.write(' {%d, %d, %d, %d, %d},\n' % (off, x0, y0, w | (0x80 if sym else 0), h))
+ f.write('};\nstatic const uint8_t SPRD[%d] = {\n' % len(out))
+ for i in range(0, len(out), 20):
+ f.write(' ' + ','.join(str(v) for v in out[i:i + 20]) + ',\n')
+ f.write('};\n')
+ print('sprite bytes:', len(out), file=sys.stderr)
+ if len(sys.argv) > 1:
+ preview(imgs, sys.argv[1])
+
+
+def shade(c, bg, color):
+ ink = (0x3A, 0x3A, 0x3A) if color == 0 else (0x14, 0x13, 0x12)
+ body = (0xF8, 0xF8, 0xF6) if color == 0 else (0x52, 0x4F, 0x4D)
+ det = ink if color == 0 else (0xA8, 0xA5, 0xA2)
+ mix = lambda a, b, t: tuple(round(x * (1 - t) + y * t) for x, y in zip(a, b))
+ return [bg, body, ink, mix(bg, ink, 0.6), mix(bg, ink, 0.3), det, mix(body, ink, 0.5), mix(body, det, 0.5)][c]
+
+
+def preview(imgs, path, Z=4):
+ """Both colors of the set on board squares, zoomed Z times."""
+ light, dark = (0xEB, 0xEC, 0xD0), (0x77, 0x95, 0x56)
+ W, Hh = 6 * N * Z, 2 * N * Z
+ pix = [[None] * W for _ in range(Hh)]
+ for color in range(2):
+ for i, (name, img) in enumerate(imgs):
+ bg = light if (i + color) % 2 == 0 else dark
+ for y in range(N * Z):
+ for x in range(N * Z):
+ pix[color * N * Z + y][i * N * Z + x] = shade(img[y // Z][x // Z], bg, color)
+ raw = b''.join(b'\0' + bytes(v for p in row for v in p) for row in pix)
+
+ def chunk(t, d):
+ return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d))
+ png = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', W, Hh, 8, 2, 0, 0, 0))
+ png += chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b'')
+ open(path, 'wb').write(png)
+
+
+if __name__ == '__main__':
+ main()