Skip to content

Commit 52c3c3b

Browse files
committed
Add Gomoku CC:Ladder (21 imported human bots + build tooling)
- 21 open-source Gomoku bots imported to CodeClash-ai/Gomoku human/gomoku/* branches (source of truth, not committed here): pure-Python minimax/MCTS/threat-search bots + JS/Java/C++ engines (lihongxun945, blackstone, blupig, ...) reimplemented in stdlib Python, each wrapped into the arena's single-file get_move(board, color) contract. Each validated 2-stage (local get_move legality + real engine.py games vs a reference). AlphaZero/CNN bots skipped (weights); zhoukangyang deferred (NN-substitute, not faithful). - configs/ablations/ladder/make_gomoku.yaml: round-robin (30 sims) for Elo ranking; README note. - scripts/ladder/: build tooling adapted to Gomoku (porting guide, validators, smoke/push scripts, RUN.md, reference bot); ports/ regenerated, not committed. Arena was already wired.
1 parent 09823d2 commit 52c3c3b

12 files changed

Lines changed: 533 additions & 0 deletions

File tree

configs/ablations/ladder/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ You can follow these steps to create your own "CC:<arena>" ladder.
1212
The tricky part is typically finding a large collection of human solutions for a particular arena.
1313
We've typically found that googling for online leaderboards or awesome-<arena> repositories (e.g. [BattleSnake](https://github.com/BattlesnakeOfficial/awesome-battlesnake)) is a good strategy.
1414

15+
### Gomoku (newly added)
16+
17+
The [CC:Gomoku](https://github.com/CodeClash-ai/Gomoku) repo hosts the human bots on `human/*`
18+
branches (like the other arenas, bot code lives only on the branches, not in this repo): 21
19+
open-source Gomoku/Gobang AIs ported into the arena's single-file `get_move(board, color)` contract
20+
— pure-Python minimax/MCTS/threat-search bots imported directly, plus JS/Java/C++ engines (e.g.
21+
lihongxun945, blackstone, blupig) reimplemented in stdlib Python — alongside a strategic starter.
22+
AlphaZero/CNN bots were skipped (need trained weights). The build tooling (porting guide,
23+
validators, smoke/push scripts, run instructions in `RUN.md`) lives in `scripts/ladder/`; it's
24+
operational one-off tooling for constructing porting-based ladders, reusable across arenas.
25+
1526
## Config layout
1627

1728
Each arena has a few kinds of config in this folder:
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Round-robin over the human Gomoku bots (ported from open-source repos + a strategic starter),
2+
# used to RANK them via Elo/Bradley-Terry -> see gomoku.yaml for the ladder.
3+
# rounds:0 = dummy players just play the baseline Gomoku game (no code edits). Colors are randomized
4+
# per game, so sims_per_round averages over first-move advantage. N bots -> N*(N-1)/2 pairs.
5+
# Bots live on CodeClash-ai/Gomoku human/* branches; Docker required.
6+
#
7+
# COST (measured ~8-9s/game -> ~4-4.5 min/pair at sims_per_round=30; 22 bots -> 231 pairs
8+
# ~= 16.5 core-hours; pairs are single-threaded so wall = core-hours / workers):
9+
# * Mac 10-core (--workers 8): ~2 h * 32-core box (-w 30): ~33 min
10+
# Matchups are decisive (frequent 30-0 sweeps), so 30 sims ranks cleanly; bump to 60 only if
11+
# the Elo confidence intervals overlap. Keep --workers <= cores-2. Resumable (skips logged pairs).
12+
# Run: uv run codeclash ladder make configs/ablations/ladder/make_gomoku.yaml --workers 8
13+
tournament:
14+
rounds: 0
15+
game:
16+
name: Gomoku
17+
sims_per_round: 30
18+
players:
19+
- agent: dummy
20+
branch_init: human/gomoku/blupig
21+
- agent: dummy
22+
branch_init: human/gomoku/canberkakcali
23+
- agent: dummy
24+
branch_init: human/gomoku/colingogogo
25+
- agent: dummy
26+
branch_init: human/gomoku/gonghailong
27+
- agent: dummy
28+
branch_init: human/gomoku/haslam22
29+
- agent: dummy
30+
branch_init: human/gomoku/hovanhoa
31+
- agent: dummy
32+
branch_init: human/gomoku/husus
33+
- agent: dummy
34+
branch_init: human/gomoku/junxiaosong_mcts
35+
- agent: dummy
36+
branch_init: human/gomoku/kali_hac
37+
- agent: dummy
38+
branch_init: human/gomoku/kevin2014123
39+
- agent: dummy
40+
branch_init: human/gomoku/lihongxun945
41+
- agent: dummy
42+
branch_init: human/gomoku/linhusp
43+
- agent: dummy
44+
branch_init: human/gomoku/marblexu
45+
- agent: dummy
46+
branch_init: human/gomoku/qwertyforce
47+
- agent: dummy
48+
branch_init: human/gomoku/rayfc_mcts
49+
- agent: dummy
50+
branch_init: human/gomoku/russell_a
51+
- agent: dummy
52+
branch_init: human/gomoku/smahot
53+
- agent: dummy
54+
branch_init: human/gomoku/tsrmkumoko
55+
- agent: dummy
56+
branch_init: human/gomoku/xkien2k4
57+
- agent: dummy
58+
branch_init: human/gomoku/zhangshun97
59+
- agent: dummy
60+
branch_init: human/gomoku/zitongmao
61+
- agent: dummy
62+
branch_init: human/muhtasham/strategic-starter
63+
prompts:
64+
game_description: Gomoku ladder

scripts/ladder/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Regenerated build artifacts — bot ports live on the CodeClash-ai/<arena> branches
2+
# (source of truth), not in this repo. Populate ports/ locally when (re)building a ladder.
3+
ports/
4+
out/

scripts/ladder/PORTING_GUIDE.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Porting a Gomoku bot to the CodeClash `get_move` contract
2+
3+
You are porting ONE open-source Gomoku / Gobang / Renju bot into a single self-contained
4+
`main.py` that defines **one function**:
5+
6+
```python
7+
def get_move(board, color) -> tuple: # returns (row, col)
8+
```
9+
10+
- `board`: a 15x15 list-of-lists of ints — `0`=empty, `1`=black, `2`=white.
11+
- `color`: `"black"` or `"white"`. Black moves first. Win = 5 in a row (H/V/either diagonal).
12+
- Return `(row, col)` of an **empty** cell (0-indexed). Returning an occupied/off-board cell or
13+
raising an exception forfeits the game, so be defensive.
14+
15+
A clean, dependency-free reference implementation lives at
16+
`scripts/ladder/examples/main.py` — read it first; mirror its structure and defensive style.
17+
18+
## Hard rules
19+
- **Stdlib only.** No numpy/pygame/tkinter/torch/etc. The arena image is plain Python 3.10 with
20+
the standard library only. Re-express any array math in pure Python lists.
21+
- **No trained weights / no NN.** If the source bot is AlphaZero/CNN/RL and has no rule-based
22+
fallback, DO NOT port it — say so in your report (it should have been filtered out already).
23+
- **One file, one function.** Everything the bot needs (search, evaluation, tables) goes inside
24+
`main.py`. Helper functions/classes are fine; `get_move` is the entry point.
25+
- **Never raise.** Wrap the body so any internal error falls back to a safe legal move (e.g. the
26+
first empty cell, or center). A crash = a forfeit.
27+
- **Be reasonably fast.** There is no hard per-move timeout in the engine, but a game plays many
28+
moves and the round-robin plays many games — keep search depth/rollouts modest (a move should
29+
return in well under a second on a 15x15 board). Cap minimax depth / MCTS iterations sensibly.
30+
31+
## What to extract from the source
32+
Most source repos wrap their AI in a GUI (pygame/tkinter), a Gomocup/piskvork stdin-stdout
33+
protocol loop, or a class with its own board representation. Ignore all of that. Find the core
34+
**"given a board, choose a move"** logic — usually a `minimax`/`negamax`/`alphabeta`, an MCTS
35+
tree search, or a threat/shape evaluation over candidate cells — and re-express it as:
36+
37+
1. Convert the arena `board` (list-of-lists, 0/1/2) to whatever the algorithm wants (often the
38+
same, sometimes it uses -1/1 or 'X'/'O' — remap). Determine `me`/`opp` from `color`.
39+
2. Run the source's move-selection over the current board.
40+
3. Map the chosen move back to a `(row, col)` int tuple and return it.
41+
42+
Keep behavior faithful: same evaluation weights, same search depth, same tie-breaks where you can.
43+
If the source relies on state across turns (opening books, transposition tables, incremental
44+
board), either recompute from `board` each call or hold it in module-level globals keyed sensibly
45+
— but the engine calls `get_move` fresh each turn with the full board, so stateless recomputation
46+
is usually simplest and correct.
47+
48+
## For non-Python sources (JS / Java / C++ — the "PROTOCOL-PORT" bucket)
49+
Reimplement the evaluation + search in Python. Prioritize faithfulness of the **evaluation
50+
function** (shape/threat scoring tables) and the **search** (depth, pruning); these determine
51+
strength. Drop language-specific perf tricks (bitboards, Zobrist/TT) unless easy — correctness and
52+
the eval shape matter more than raw speed for the ladder.
53+
54+
## Deliverable
55+
Write your port to `scripts/ladder/ports/<name>.py` (see the batch prompt for the exact filename,
56+
e.g. `colingogogo.py`). It must:
57+
- `python3 -c "import ast; ast.parse(open(FILE).read())"` cleanly,
58+
- define a top-level `get_move(board, color)`,
59+
- return a legal `(row, col)` on an empty starter board and mid-game, never raising.
60+
61+
Keep a short module docstring naming the source (repo + author + algorithm) and noting any
62+
simplifications/dropped features (e.g. "dropped Zobrist TT", "MCTS iters capped at 2000").

scripts/ladder/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Ladder build tooling (Gomoku)
2+
3+
Operational one-off scripts for constructing a **porting-based** CC:Ladder — one where the
4+
human bots are open-source agents that must be ported into the arena's single-file submission
5+
contract before they can be ranked. This branch targets **Gomoku** (`main.py` defining
6+
`get_move(board, color) -> (row, col)`); the same workflow was used for SCML OneShot.
7+
8+
This is scaffolding, not product code — nothing under `codeclash/` imports it. The durable
9+
outputs of a build are: the `human/*` branches on `CodeClash-ai/Gomoku` (the bots) and the
10+
`configs/ablations/ladder/*.yaml` configs. Ports themselves are **not** committed here (see
11+
`.gitignore`); they live on the branches.
12+
13+
## Files
14+
- `PORTING_GUIDE.md` — the `get_move` contract + how to port a source bot. Hand this to a porting
15+
agent (with `examples/main.py` as the worked reference).
16+
- `examples/main.py` — a clean, dependency-free reference bot (win/block/heuristic). Used as the
17+
worked porting example and the stage-2 smoke opponent.
18+
- `examples/gomoku_smoke.yaml` — a 2-player arena smoke config (branch bot vs default).
19+
- `validate_ports.py` — stage 1: local syntax/import/`get_move` legality check over `ports/*.py`.
20+
- `run_smoke_all.sh` — stage 2: play every stage-1 pass vs the reference bot through the real
21+
engine in Docker; confirm each plays full games without erroring/hanging. Writes `ports/_stage2.json`.
22+
- `smoke_gomoku.sh` — quick single-pair smoke (reference vs the arena default) through the image.
23+
- `push_branches.sh` — push each stage-2-healthy port to `CodeClash-ai/Gomoku` as a
24+
`human/gomoku/<slug>` branch (submission `main.py`; dedupes identical content).
25+
- `RUN.md` — how to run the round-robin (`ladder make`) + Elo ranking (cheap — runs locally).
26+
27+
## Typical flow
28+
1. Populate `ports/<slug>.py` (fan out porting agents with `PORTING_GUIDE.md`).
29+
2. `python3 scripts/ladder/validate_ports.py` → stage 1
30+
3. `bash scripts/ladder/run_smoke_all.sh` → stage 2 (needs Docker)
31+
4. `bash scripts/ladder/push_branches.sh` → push healthy ports to branches
32+
5. Rank + assemble configs — see `RUN.md`.

scripts/ladder/RUN.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Running the Gomoku round-robin (Elo ranking)
2+
3+
Rank the human bots in `configs/ablations/ladder/make_gomoku.yaml` via all-pairs PvP, then fit
4+
Elo. Gomoku games are fast and stdlib-only, so — unlike SCML — this runs comfortably **on a
5+
laptop** (no AWS needed). The bots already live on `CodeClash-ai/Gomoku` `human/*` branches;
6+
`branch_init` fetches them at runtime, so you only need this repo branch + Docker + a GitHub token.
7+
8+
## Prerequisites
9+
- Docker running, git, `uv`, and a token for `CodeClash-ai/Gomoku` (public → `gh auth token` works).
10+
11+
## Step 0 — pre-build the arena image once (avoids a build stampede under -w N)
12+
```bash
13+
docker build -t codeclash/gomoku -f codeclash/arenas/gomoku/Gomoku.Dockerfile .
14+
bash scripts/ladder/smoke_gomoku.sh # sanity: reference vs default, prints PASS
15+
```
16+
17+
## Step 1 — round-robin
18+
`N` bots → `N*(N-1)/2` pairs; each pair is a short match, so this is quick. Keep
19+
`--workers ≈ cores-2`.
20+
```bash
21+
GITHUB_TOKEN=$(gh auth token) \
22+
uv run codeclash ladder make configs/ablations/ladder/make_gomoku.yaml --workers 6
23+
```
24+
Resumable — reruns skip pairs already logged under `logs/ladder/Gomoku/`.
25+
26+
## Step 2 — rank
27+
```bash
28+
python -m codeclash.analysis.metrics.elo -d logs/ladder/Gomoku --output-dir assets/gomoku_elo
29+
```
30+
Prints the Bradley-Terry/Elo ordering and writes `assets/gomoku_elo/elo_results.log`.
31+
32+
## Step 3 — assemble the ladder configs (once you have the ranking)
33+
1. `configs/ablations/ladder/rungs/gomoku.yaml` — ranked opponents, weakest first, strongest last.
34+
2. `configs/ablations/ladder/gomoku.yaml` — climber `player` + `ladder: !include ablations/ladder/rungs/gomoku.yaml`
35+
+ a `ladder_rules` block (model on `battlesnake.yaml`).
36+
3. Optional `gomoku__<model>.yaml` per-model variants.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Arena smoke test for the Gomoku ladder: run a human-branch bot through the REAL arena
2+
# (Docker build -> clone CodeClash-ai/Gomoku -> branch_init checkout -> validate_code -> engine.py)
3+
# against the default bot.
4+
# GITHUB_TOKEN=$(gh auth token) uv run codeclash run scripts/ladder/examples/gomoku_smoke.yaml
5+
tournament:
6+
rounds: 1
7+
game:
8+
name: Gomoku
9+
sims_per_round: 6
10+
players:
11+
- agent: dummy
12+
name: starter
13+
branch_init: human/muhtasham/strategic-starter
14+
- agent: dummy
15+
name: default
16+
prompts:
17+
game_description: Gomoku ladder smoke test

scripts/ladder/examples/main.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Reference Gomoku bot for the CodeClash arena contract.
2+
3+
The arena calls ONE function per turn:
4+
5+
get_move(board, color) -> (row, col)
6+
7+
* board: 15x15 list-of-lists of ints, 0=empty, 1=black, 2=white.
8+
* color: "black" or "white" (black moves first).
9+
* return: (row, col) of an EMPTY cell. Win = 5 in a row (H/V/diagonal).
10+
11+
This reference is a compact, dependency-free heuristic used as the porting example
12+
and the smoke-test opponent: (1) win now if possible, (2) block the opponent's
13+
immediate win, (3) otherwise score empty cells near existing stones by the line
14+
potential they create for me and deny to the opponent, and play the best.
15+
"""
16+
17+
SIZE = 15
18+
_DIRS = [(1, 0), (0, 1), (1, 1), (1, -1)]
19+
20+
21+
def _stones(color):
22+
me = 1 if color == "black" else 2
23+
return me, (2 if me == 1 else 1)
24+
25+
26+
def _on(r, c):
27+
return 0 <= r < SIZE and 0 <= c < SIZE
28+
29+
30+
def _makes_five(board, r, c, stone):
31+
"""Would playing `stone` at (r,c) complete 5+ in a row?"""
32+
for dr, dc in _DIRS:
33+
n = 1
34+
for sgn in (1, -1):
35+
rr, cc = r + dr * sgn, c + dc * sgn
36+
while _on(rr, cc) and board[rr][cc] == stone:
37+
n += 1
38+
rr += dr * sgn
39+
cc += dc * sgn
40+
if n >= 5:
41+
return True
42+
return False
43+
44+
45+
def _line_score(board, r, c, stone):
46+
"""Rough potential of playing `stone` at (r,c): sum over directions of the
47+
contiguous run length it extends, weighted super-linearly."""
48+
total = 0
49+
for dr, dc in _DIRS:
50+
run = 0
51+
for sgn in (1, -1):
52+
rr, cc = r + dr * sgn, c + dc * sgn
53+
while _on(rr, cc) and board[rr][cc] == stone:
54+
run += 1
55+
rr += dr * sgn
56+
cc += dc * sgn
57+
total += (run + 1) ** 2
58+
return total
59+
60+
61+
def _candidates(board):
62+
"""Empty cells adjacent (incl. diagonally) to any stone; center if board empty."""
63+
cells = set()
64+
any_stone = False
65+
for r in range(SIZE):
66+
for c in range(SIZE):
67+
if board[r][c] != 0:
68+
any_stone = True
69+
for dr in (-1, 0, 1):
70+
for dc in (-1, 0, 1):
71+
rr, cc = r + dr, c + dc
72+
if _on(rr, cc) and board[rr][cc] == 0:
73+
cells.add((rr, cc))
74+
if not any_stone:
75+
return [(SIZE // 2, SIZE // 2)]
76+
return list(cells)
77+
78+
79+
def get_move(board, color):
80+
me, opp = _stones(color)
81+
cands = _candidates(board)
82+
83+
# 1. win now
84+
for r, c in cands:
85+
if _makes_five(board, r, c, me):
86+
return (r, c)
87+
# 2. block opponent's win
88+
for r, c in cands:
89+
if _makes_five(board, r, c, opp):
90+
return (r, c)
91+
# 3. best heuristic cell (my potential + defensive value)
92+
best, best_rc = -1, cands[0]
93+
for r, c in cands:
94+
s = _line_score(board, r, c, me) + 0.9 * _line_score(board, r, c, opp)
95+
if s > best:
96+
best, best_rc = s, (r, c)
97+
return best_rc

scripts/ladder/push_branches.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
# Push every stage-2-healthy port to CodeClash-ai/Gomoku as a human/gomoku/<slug> branch,
3+
# with the port placed as the arena submission file main.py.
4+
# Port "colingogogo.py" -> branch "human/gomoku/colingogogo". Dedupes identical content.
5+
# bash scripts/ladder/push_branches.sh
6+
set -euo pipefail
7+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
8+
PORTS="$REPO_ROOT/scripts/ladder/ports"
9+
ARENA_REPO="CodeClash-ai/Gomoku"
10+
SUBMISSION="main.py"
11+
SKIP="zhoukangyang" # NN/AlphaZero source with no rule-based path; the port is a uniform-prior
12+
# UCT substitute (not faithful; duplicates tsrmkumoko) -> defer
13+
14+
TMP=$(mktemp -d)
15+
git clone -q "https://x-access-token:$(gh auth token)@github.com/${ARENA_REPO}.git" "$TMP"
16+
cd "$TMP"
17+
git checkout -q main
18+
19+
SEEN=$(mktemp)
20+
pushed=0; skipped=0
21+
PY="$(command -v python3 || command -v python)"
22+
OKS=$("$PY" -c "import json;d=json.load(open('$PORTS/_stage2.json'));print('\n'.join(k for k,v in sorted(d.items()) if v.get('ran') and v.get('errors',1)==0))")
23+
24+
for stem in $OKS; do
25+
stem="${stem%.py}"
26+
case " $SKIP " in *" $stem "*) echo "SKIP $stem (deferred)"; skipped=$((skipped+1)); continue;; esac
27+
h=$(shasum "$PORTS/$stem.py" | awk '{print $1}')
28+
if grep -q "^$h " "$SEEN"; then echo "SKIP $stem (dup of $(grep "^$h " "$SEEN" | awk '{print $2}'))"; skipped=$((skipped+1)); continue; fi
29+
echo "$h $stem" >> "$SEEN"
30+
branch="human/gomoku/$stem"
31+
git checkout -q main
32+
git checkout -q -B "$branch"
33+
cp "$PORTS/$stem.py" "$SUBMISSION"
34+
git add "$SUBMISSION"
35+
git -c user.email=player@codeclash.com -c user.name="CodeClash" commit -qm "Import $stem (Gomoku ladder)"
36+
git push -q -f -u origin "$branch" 2>/dev/null
37+
echo "PUSH $branch"
38+
pushed=$((pushed+1))
39+
done
40+
cd "$REPO_ROOT"; rm -rf "$TMP"
41+
echo "=== pushed $pushed, skipped $skipped ==="

0 commit comments

Comments
 (0)