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 @@ +

+ Playing against a bot +

+ +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. + +

+ Solving a puzzle + Two-player clock +

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 icon +

+ +

NumChess

+ +

+ Chess for the NumWorks N0120 calculator.
+ 12 bots, endless puzzles and a two-player clock, in a native app of about 20 KB. +

+ +

+ Latest release + Build status + Installed size: 20 KB +

+ +

+ Download chess.nwa +  ·  Install +  ·  Controls +  ·  Build +

+ +

+ Choosing a bot and playing the Italian Game against Nova +

+ +## 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
Main menu
A game in progress
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. + +

+ All 12 bots with their Elo ratings +

+ + + + + + + + + + + + + + + + + + +
Scrolling through the bots
Choose a bot and your color
Atlas selected
Atlas, the strongest bot, thinks up to 2.5 s per move
Legal moves shown as dots and capture rings
Legal moves as dots, captures as rings
King in check glowing red
Check glow, last-move highlight, captured pieces with material count
Promotion picker
Promotion picker
Game over card
Result card with rematch
Pause menu
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. + +

+ Solving a mate in 2 puzzle +

+ +* **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
Puzzle modes
Mate in 1
Mate in 1: the pill shows who is to move
Mate in 3 as Black
Mate in 3 as Black: the board flips for you
Best move puzzle
Best move: find the knight fork
Hint highlight
Hint
Wrong move
Wrong move: it's taken back and your rating drops
Solved
Solved
Puzzle pause menu
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. + +

+ Two players with a 3+2 clock +

+ + + + + + + +
Time controls
1+0 to 30+0, or no clock
Clocks
The active clock is lit
Low time
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 + +

+ The piece set, zoomed 4x +

+ +| | | +|---|---| +| 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()