refactor(rithmomachia): Update board setup to authoritative CSV layout

Replace previous board setup with traditional 24-piece formation derived
from authoritative CSV source. Update SPEC.md to reflect correct layout.

Changes:
- 24 pieces per side (7 Squares, 8 Triangles, 8 Circles, 1 Pyramid)
- Black pieces in columns A-D (left side)
- White pieces in columns M-P (right side)
- Black Pyramid at B8, White Pyramid at O2
- 8 empty columns in middle (E-L battlefield)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-10-31 10:52:57 -05:00
parent 915c10a066
commit 0471da598d
2 changed files with 131 additions and 105 deletions

View File

@ -28,13 +28,13 @@ This spec aims for: fully deterministic setup, no ambiguities, consistent networ
## 2) Pieces and movement
Each side has **25 pieces**:
Each side has **24 pieces**:
* **12 Circles (C)** — "light" pieces
* **8 Circles (C)** — "light" pieces
Movement: **diagonal, any distance**, no jumping (like a bishop).
* **6 Triangles (T)** — "medium" pieces
* **8 Triangles (T)** — "medium" pieces
Movement: **orthogonal, any distance**, no jumping (like a rook).
* **6 Squares (S)** — "heavy" pieces
* **7 Squares (S)** — "heavy" pieces
Movement: **queen-like, any distance**, no jumping (orthogonal or diagonal).
* **1 Pyramid (P)** — "royal" piece
Movement: **king-like, 1 step** in any direction (8-neighborhood).
@ -54,20 +54,20 @@ This is the **traditional Rithmomachia** ("The Philosophers' Game") setup, where
* **Squares** → Square numbers and composites; move like **queens** (orthogonal + diagonal)
* **Pyramids** → Composite/sum pieces with multiple faces; move like **kings** (1 step any direction)
### 3.2 Black values (higher descending values)
### 3.2 Black values (traditional layout)
**Total: 25 pieces**
* **Squares (6):** `361` (19²), `225` (15²), `121` (11²), `120`, `64` (8²), `49` (7²)
* **Triangles (9):** `90`, `66` (11th triangular), `64`, `56` (7th triangular), `36` (6²), `30` (5th triangular), `16`, `12`, `9`
* **Circles (9):** `81` (9²), `49` (7²), `25` (5²), `16`, `12`, `9`, `4`, `3`, `2`
**Total: 24 pieces**
* **Squares (7):** `28` (×2), `66` (×2), `120`, `225` (15²), `361` (19²)
* **Triangles (8):** `12`, `16` (4²), `30`, `36` (6²), `56`, `64` (8²), `90`, `100` (10²)
* **Circles (8):** `3`, `5`, `7`, `9` (×2), `25` (5²), `49` (7²), `81` (9²)
* **Pyramid (1):** `[36, 25, 16, 4]` (faces: 6², 5², 4², 2²)
### 3.3 White values (smaller units, geometric harmony)
### 3.3 White values (traditional layout)
**Total: 25 pieces**
* **Squares (8):** `289` (17²), `169` (13²), `153`, `81` (9²), `45`, `25` (5²), `18`, `15`
* **Triangles (8):** `72`, `49` (7²), `42` (6th triangular), `20` (4th triangular), `9`, `6`, `5`, `4`
* **Circles (8):** `64` (8²), `36` (6²), `25`, `16` (4²), `8`, `6`, `4` (×2), `2` (×2)
**Total: 24 pieces**
* **Squares (7):** `15`, `25` (5²), `45`, `81` (9²), `153`, `169` (13²), `289` (17²)
* **Triangles (8):** `6`, `9`, `20` (×2), `25` (5²), `72`, `81` (9²) (note: one T with value 72 appears twice in column O)
* **Circles (8):** `2`, `4` (×2), `6`, `8`, `16` (×2), `64` (8²)
* **Pyramid (1):** `[64, 49, 36, 25]` (faces: 8², 7², 6², 5²)
> **Philosophical note:** The initial layout visually encodes proportionality—large composite figurates on outer edges, smaller simple numbers inside. Numbers on each side form progressions that enable arithmetical, geometrical, and harmonical victories. For relations and Pyramid captures, the Pyramid's **face value** is chosen by the owner at capture time.
@ -77,65 +77,78 @@ This is the **traditional Rithmomachia** ("The Philosophers' Game") setup, where
## 4) Initial setup — Traditional formation
**SYMMETRIC VERTICAL LAYOUT** — The board is **8 rows × 16 columns** with:
- **BLACK (left side)**: Columns **A, B, C** (outer to inner)
- **WHITE (right side)**: Columns **N, O, P** (inner to outer)
- **Battlefield (middle)**: Columns **D through M** (10 empty columns)
- **BLACK (left side)**: Columns **A, B, C, D**
- **WHITE (right side)**: Columns **M, N, O, P**
- **Battlefield (middle)**: Columns **E through L** (8 empty columns)
This is the **classical symmetric formation** where large composite numbers occupy the outer edges, progressing to smaller geometric bases toward the inside. The layout encodes the mathematical philosophy: larger figurates command the flanks, while nimble units infiltrate the center.
This is the **classical symmetric formation** from authoritative historical sources. The layout places larger values on outer edges (columns A and P) with smaller values toward the interior, encoding mathematical progressions that enable harmony victories.
### BLACK Setup (Left side — columns A, B, C)
### BLACK Setup (Left side — columns A, B, C, D)
**Column A** (Outer edge — Large squares and triangles):
**Column A** (Outer edge — Sparse squares):
```
A1: S(49) A2: S(121) A3: T(36) A4: T(30)
A5: T(56) A6: S(120) A7: S(225) A8: S(361)
A1: S(28) A2: S(66) A3: empty A4: empty
A5: empty A6: empty A7: S(225) A8: S(361)
```
**Column B** (Middle — Mixed pieces + Pyramid):
**Column B** (Mixed with Pyramid at B8):
```
B1: empty B2: T(66) B3: C(9) B4: C(25)
B5: C(49) B6: T(64) B7: C(81) B8: P[36,25,16,4]
B1: S(28) B2: S(66) B3: T(36) B4: T(30)
B5: T(56) B6: T(64) B7: S(120) B8: P[36,25,16,4]
```
**Column C** (Inner edge — Small units):
**Column C** (Triangles and circles):
```
C1: T(16) C2: T(12) C3: C(9) C4: C(7)
C5: C(5) C6: C(3) C7: T(90) C8: T(9)
C1: T(16) C2: T(12) C3: C(9) C4: C(25)
C5: C(49) C6: C(81) C7: T(90) C8: T(100)
```
### WHITE Setup (Right side — columns N, O, P)
**Column N** (Inner edge — Small units):
**Column D** (Inner edge — Small circles, sparse):
```
N1: T(4) N2: C(2) N3: C(6) N4: C(8)
N5: C(4) N6: C(2) N7: T(6) N8: T(9)
D1: empty D2: empty D3: C(3) D4: C(5)
D5: C(7) D6: C(9) D7: empty D8: empty
```
**Column O** (Middle — Mixed pieces + Pyramid):
### WHITE Setup (Right side — columns M, N, O, P)
**Column M** (Inner edge — Small circles, sparse):
```
O1: S(153) O2: P[64,49,36,25] O3: C(25) O4: C(36)
O5: C(64) O6: C(16) O7: C(4) O8: S(169)
M1: empty M2: empty M3: C(8) M4: C(6)
M5: C(4) M6: C(2) M7: empty M8: empty
```
**Column P** (Outer edge — Large squares and triangles):
**Column N** (Triangles and circles):
```
P1: S(289) P2: S(81) P3: T(20) P4: T(42)
P5: T(49) P6: T(72) P7: S(45) P8: S(25)
N1: T(81) N2: T(72) N3: C(64) N4: C(16)
N5: C(16) N6: C(4) N7: T(6) N8: T(9)
```
**Column O** (Mixed with Pyramid at O2):
```
O1: S(153) O2: P[64,49,36,25] O3: T(72) O4: T(20)
O5: T(20) O6: T(25) O7: S(45) O8: S(15)
```
**Column P** (Outer edge — Sparse squares):
```
P1: S(289) P2: S(169) P3: empty P4: empty
P5: empty P6: empty P7: S(81) P8: S(25)
```
### Piece Count Summary
**BLACK**: 6 Squares, 9 Triangles, 9 Circles, 1 Pyramid = **25 pieces**
**WHITE**: 8 Squares, 8 Triangles, 8 Circles, 1 Pyramid = **25 pieces**
**BLACK**: 7 Squares, 8 Triangles, 8 Circles, 1 Pyramid = **24 pieces**
**WHITE**: 7 Squares, 8 Triangles, 8 Circles, 1 Pyramid = **24 pieces**
### Strategic layout philosophy
* **Outer edges (A and P)**: Heavy squares (361, 289, 225, 169, etc.) command the flanks
* **Middle columns (B and O)**: Mix of powers with the Pyramids (royal pieces) at B8 (Black) and O2 (White)
* **Inner edges (C and N)**: Nimble circles and small triangles (216) for rapid infiltration
* **Central battlefield (DM)**: 10 empty columns provide space for mathematical maneuvering
* **Outer edges (A and P)**: Heavy squares (361, 289, 225, 169, etc.) command the flanks with sparse placement
* **Secondary columns (B and O)**: Dense formations with Pyramids (royal pieces) at B8 (Black) and O2 (White)
* **Tertiary columns (C and N)**: Full ranks of mixed triangles and circles
* **Inner edges (D and M)**: Small circles (29) for tactical infiltration, sparse placement
* **Central battlefield (EL)**: 8 empty columns provide space for mathematical maneuvering
The alternating dark/light visual pattern is purely aesthetic; movement is grid-based, not color-based. White moves first.
Some pieces appear with duplicate values (e.g., A1 and B1 both have S(28)), reflecting the traditional layout's mathematical symmetries. White moves first.
---
@ -532,15 +545,18 @@ interface HarmonyDeclaration {
## Implementation Status
Last updated: 2025-10-29
Last updated: 2025-10-31
The current implementation in `src/arcade-games/rithmomachia/` follows this spec:
- **Board setup**: ✅ VERTICAL layout (§4) - BLACK on left (columns A-C), WHITE on right (columns M-P)
- **Board setup**: ✅ VERTICAL layout (§4) - BLACK on left (columns A-D), WHITE on right (columns M-P)
- Authoritative CSV-derived layout (parsed from historical sources)
- 24 pieces per side (7 Squares, 8 Triangles, 8 Circles, 1 Pyramid)
- Black Pyramid at B8, White Pyramid at O2
- **Piece rendering**: ✅ SVG-based with precise color control (PieceRenderer.tsx)
- BLACK pieces: Dark fill (#1a1a1a) with black stroke
- WHITE pieces: Light fill (#ffffff) with gray stroke
- **Piece values**: ✅ Match reference board image exactly (24 pieces per side)
- **Piece values**: ✅ Match authoritative CSV exactly (§3.2, §3.3)
- **Movement validation**: ✅ Implemented in `Validator.ts` following geometric rules
- **Capture system**: ✅ Relation-based captures per §6
- **Harmony system**: ✅ Progression detection and validation per §7

View File

@ -4,56 +4,62 @@ import type { Color, Piece } from '../types'
* Generate the initial board setup for traditional Rithmomachia.
* Returns a Record of piece.id Piece.
*
* Layout: VERTICAL - BLACK on left (columns A-C), WHITE on right (columns N-P)
* This is the classical symmetric formation with 25 pieces per side.
* Layout generated from authoritative CSV (rotated 90° CCW):
* - BLACK on left (columns A-D)
* - WHITE on right (columns M-P)
* - 24 pieces per side (48 total)
*/
export function createInitialBoard(): Record<string, Piece> {
const pieces: Record<string, Piece> = {}
// === BLACK PIECES (Left side: columns A, B, C) ===
// Traditional setup: large figurates on outer edges, small units inside
// === BLACK PIECES (Left side) ===
// Layout from CSV: portrait → rotated 90° CCW for game orientation
// Column A (Outer edge - Large squares and triangles)
// Column A: Outer edge (sparse)
const blackColumnA = [
{ type: 'S', value: 49, square: 'A1' },
{ type: 'S', value: 121, square: 'A2' },
{ type: 'T', value: 36, square: 'A3' },
{ type: 'T', value: 30, square: 'A4' },
{ type: 'T', value: 56, square: 'A5' },
{ type: 'S', value: 120, square: 'A6' }, // was T(64) - moved to outer rim
{ type: 'S', value: 28, square: 'A1' },
{ type: 'S', value: 66, square: 'A2' },
{ type: 'S', value: 225, square: 'A7' },
{ type: 'S', value: 361, square: 'A8' },
] as const
// Column B (Middle - Mixed pieces + Pyramid)
// Column B: Mixed with Pyramid at B8
const blackColumnB = [
// B1: empty
{ type: 'T', value: 66, square: 'B2' },
{ type: 'C', value: 9, square: 'B3' },
{ type: 'C', value: 25, square: 'B4' },
{ type: 'C', value: 49, square: 'B5' },
{ type: 'T', value: 64, square: 'B6' }, // was C(81) - now has T(64) from A6
{ type: 'C', value: 81, square: 'B7' }, // was S(120) - now has C(81) from B6
// B8 is Pyramid (see below)
{ type: 'S', value: 28, square: 'B1' },
{ type: 'S', value: 66, square: 'B2' },
{ type: 'T', value: 36, square: 'B3' },
{ type: 'T', value: 30, square: 'B4' },
{ type: 'T', value: 56, square: 'B5' },
{ type: 'T', value: 64, square: 'B6' },
{ type: 'S', value: 120, square: 'B7' },
// B8: Pyramid (see below)
] as const
// Column C (Inner edge - Small units)
// Column C: Triangles and circles
const blackColumnC = [
{ type: 'T', value: 16, square: 'C1' },
{ type: 'T', value: 12, square: 'C2' },
{ type: 'C', value: 9, square: 'C3' }, // was 3 - corrected to match reference
{ type: 'C', value: 7, square: 'C4' }, // was 4 - corrected to match reference
{ type: 'C', value: 5, square: 'C5' }, // was 2 - corrected to match reference
{ type: 'C', value: 3, square: 'C6' }, // was 12 - corrected to match reference
{ type: 'C', value: 9, square: 'C3' },
{ type: 'C', value: 25, square: 'C4' },
{ type: 'C', value: 49, square: 'C5' },
{ type: 'C', value: 81, square: 'C6' },
{ type: 'T', value: 90, square: 'C7' },
{ type: 'T', value: 9, square: 'C8' },
{ type: 'T', value: 100, square: 'C8' },
] as const
// Column D: Small circles (sparse)
const blackColumnD = [
{ type: 'C', value: 3, square: 'D3' },
{ type: 'C', value: 5, square: 'D4' },
{ type: 'C', value: 7, square: 'D5' },
{ type: 'C', value: 9, square: 'D6' },
] as const
let blackSquareCount = 0
let blackTriangleCount = 0
let blackCircleCount = 0
for (const piece of [...blackColumnA, ...blackColumnB, ...blackColumnC]) {
for (const piece of [...blackColumnA, ...blackColumnB, ...blackColumnC, ...blackColumnD]) {
let id: string
let count: number
if (piece.type === 'S') {
@ -87,42 +93,46 @@ export function createInitialBoard(): Record<string, Piece> {
captured: false,
}
// === WHITE PIECES (Right side: columns N, O, P) ===
// Traditional setup mirrors Black with inverse ratios
// === WHITE PIECES (Right side) ===
// Layout from CSV: portrait → rotated 90° CCW for game orientation
// Column N (Inner edge - Small units)
const whiteColumnN = [
{ type: 'T', value: 4, square: 'N1' },
{ type: 'C', value: 2, square: 'N2' },
{ type: 'C', value: 6, square: 'N3' },
{ type: 'C', value: 8, square: 'N4' },
{ type: 'C', value: 4, square: 'N5' },
{ type: 'C', value: 2, square: 'N6' },
{ type: 'T', value: 6, square: 'N7' },
{ type: 'T', value: 9, square: 'N8' }, // was 5 - corrected to match reference
// Column M: Small circles (sparse)
const whiteColumnM = [
{ type: 'C', value: 8, square: 'M3' },
{ type: 'C', value: 6, square: 'M4' },
{ type: 'C', value: 4, square: 'M5' },
{ type: 'C', value: 2, square: 'M6' },
] as const
// Column O (Middle - Mixed pieces + Pyramid)
// Column N: Triangles and circles
const whiteColumnN = [
{ type: 'T', value: 81, square: 'N1' },
{ type: 'T', value: 72, square: 'N2' },
{ type: 'C', value: 64, square: 'N3' },
{ type: 'C', value: 16, square: 'N4' },
{ type: 'C', value: 16, square: 'N5' },
{ type: 'C', value: 4, square: 'N6' },
{ type: 'T', value: 6, square: 'N7' },
{ type: 'T', value: 9, square: 'N8' },
] as const
// Column O: Mixed with Pyramid at O2
const whiteColumnO = [
{ type: 'S', value: 153, square: 'O1' },
// O2 is Pyramid (see below) - moved from O7
{ type: 'C', value: 25, square: 'O3' }, // shifted down from O2
{ type: 'C', value: 36, square: 'O4' }, // shifted down from O3
{ type: 'C', value: 64, square: 'O5' }, // shifted down from O4
{ type: 'C', value: 16, square: 'O6' }, // shifted down from O5
{ type: 'C', value: 4, square: 'O7' }, // shifted down from O6
{ type: 'S', value: 169, square: 'O8' },
// O2: Pyramid (see below)
{ type: 'T', value: 72, square: 'O3' },
{ type: 'T', value: 20, square: 'O4' },
{ type: 'T', value: 20, square: 'O5' },
{ type: 'T', value: 25, square: 'O6' },
{ type: 'S', value: 45, square: 'O7' },
{ type: 'S', value: 15, square: 'O8' },
] as const
// Column P (Outer edge - Large squares and triangles)
// Column P: Outer edge (sparse)
const whiteColumnP = [
{ type: 'S', value: 289, square: 'P1' },
{ type: 'S', value: 81, square: 'P2' },
{ type: 'T', value: 20, square: 'P3' },
{ type: 'T', value: 42, square: 'P4' },
{ type: 'T', value: 49, square: 'P5' },
{ type: 'T', value: 72, square: 'P6' },
{ type: 'S', value: 45, square: 'P7' },
{ type: 'S', value: 169, square: 'P2' },
{ type: 'S', value: 81, square: 'P7' },
{ type: 'S', value: 25, square: 'P8' },
] as const
@ -130,7 +140,7 @@ export function createInitialBoard(): Record<string, Piece> {
let whiteTriangleCount = 0
let whiteCircleCount = 0
for (const piece of [...whiteColumnN, ...whiteColumnO, ...whiteColumnP]) {
for (const piece of [...whiteColumnM, ...whiteColumnN, ...whiteColumnO, ...whiteColumnP]) {
let id: string
let count: number
if (piece.type === 'S') {
@ -153,7 +163,7 @@ export function createInitialBoard(): Record<string, Piece> {
}
}
// White Pyramid at O2 (moved lower to match reference image)
// White Pyramid at O2
pieces.W_P_01 = {
id: 'W_P_01',
color: 'W',