feat(rithmomachia): Update harmony system to classical three-piece proportions
Updated harmony validation to match classical Rithmomachia rules using three-piece proportions (A-M-B structure) with integer arithmetic formulas: - SPEC.md §7: Rewrote harmony section with classical three-piece rules - AP (Arithmetic): 2M = A + B (middle is arithmetic mean) - GP (Geometric): M² = A·B (middle is geometric mean) - HP (Harmonic): 2AB = M(A+B) (middle is harmonic mean) - Added layout constraints: adjacent, equalSpacing, collinear - Added common harmony triads reference list - harmonyValidator.ts: Complete rewrite for three-piece validation - Collinearity checking (straight line requirement) - Spatial middle piece detection - Integer proportion formulas (no division needed) - Layout modes: adjacent (all distance 1), equalSpacing (equal 1-2), collinear (any) - Fixed coordinate access: use file/rank instead of col/row - types.ts: Updated HarmonyDeclaration.params structure - Changed from v/d/r/n fields to a/m/b (first/middle/last values) - Matches classical three-piece proportion structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
67df4d244c
commit
08c97620f5
|
|
@ -180,20 +180,68 @@ If, **after your movement**, an **enemy piece** sits on a square such that a rel
|
|||
|
||||
## 7) Harmony (progression) victory
|
||||
|
||||
On your turn (after movement/captures), you may **declare Harmony** if you have **≥ 3** of your pieces **entirely within the opponent's half** (White in rows 5–8, Black in rows 1–4) whose **values form an exact progression** of one of these types:
|
||||
**Harmony** is both the theme of Rithmomachia and a special way to win. On your turn (after movement/captures), you may **declare Harmony** if you arrange three of your pieces in the **opponent's half** (White in rows 5–8, Black in rows 1–4) so their **values stand in a classical proportion**.
|
||||
|
||||
* **Arithmetic:** values `v, v+d, v+2d, …` with `d > 0`
|
||||
* **Geometric:** values `v, v·r, v·r², …` with integer `r ≥ 2`
|
||||
* **Harmonic:** reciprocals form an arithmetic progression (i.e., `1/v` is arithmetic); equivalently, `v, v·(n/(n-1)), v·(n/(n-2)), …` for some integer `n` (server should validate via reciprocals to avoid rounding)
|
||||
### 7.1 Three types of harmony (three-piece structure: A–M–B)
|
||||
|
||||
All harmonies use **three pieces** where M is the middle piece (spatially between A and B on the board):
|
||||
|
||||
* **Arithmetic Proportion (AP)**: the middle is the arithmetic mean
|
||||
- **Condition:** `2M = A + B`
|
||||
- **Example:** 6, 9, 12 (since 2·9 = 6 + 12 = 18)
|
||||
|
||||
* **Geometric Proportion (GP)**: the middle is the geometric mean
|
||||
- **Condition:** `M² = A · B`
|
||||
- **Example:** 6, 12, 24 (since 12² = 6·24 = 144)
|
||||
|
||||
* **Harmonic Proportion (HP)**: the middle is the harmonic mean
|
||||
- **Condition:** `2AB = M(A + B)` (equivalently, 1/A, 1/M, 1/B forms an AP)
|
||||
- **Examples:**
|
||||
- 6, 8, 12 (since 2·6·12 = 8·(6+12) = 144)
|
||||
- 10, 12, 15 (since 2·10·15 = 12·(10+15) = 300)
|
||||
- 8, 12, 24 (since 2·8·24 = 12·(8+24) = 384)
|
||||
|
||||
> **Tip:** Use these integer equalities for validation—no division or rounding needed!
|
||||
|
||||
### 7.2 Board layout constraints
|
||||
|
||||
The three pieces must be arranged in a **straight line** (row, column, or diagonal) with one of these spacing rules:
|
||||
|
||||
1. **Straight & adjacent** (default): Three consecutive squares in order A–M–B
|
||||
2. **Straight with equal spacing**: Same as above, but one empty square between each neighbor (still collinear)
|
||||
3. **Collinear anywhere**: Pieces on the same line in correct numeric order, with any spacing
|
||||
|
||||
**Default for this implementation:** Straight & adjacent (option 1)
|
||||
|
||||
### 7.3 Common harmony triads (for reference)
|
||||
|
||||
**Arithmetic:**
|
||||
- (6, 9, 12), (8, 12, 16), (5, 7, 9), (4, 6, 8)
|
||||
|
||||
**Geometric:**
|
||||
- (4, 8, 16), (3, 9, 27), (2, 8, 32), (5, 25, 125)
|
||||
|
||||
**Harmonic:**
|
||||
- (3, 4, 6), (4, 6, 12), (6, 8, 12), (10, 12, 15), (8, 12, 24), (6, 10, 15)
|
||||
|
||||
### 7.4 Declaring and winning
|
||||
|
||||
**Rules:**
|
||||
|
||||
* Pieces in the set must be **distinct** and **on distinct squares**.
|
||||
* Order doesn't matter; the set must be **exact** (no extra elements required).
|
||||
* **Pyramid face**: When a Pyramid is included, you must **fix** a face value for the duration of the check.
|
||||
* **Persistence:** Your declared Harmony must **survive the opponent's next full turn** (they can try to break it by moving/capturing). If, when your next turn begins, the Harmony still exists (same set or **any valid set** of ≥3 on the enemy half), **you win immediately**.
|
||||
* Pieces must be **distinct** and on **distinct squares**
|
||||
* All three must be **entirely within opponent's half**
|
||||
* **Pyramid face**: When a Pyramid is included, you must **fix** a face value for the duration of the check
|
||||
* **Persistence:** Your declared Harmony must **survive the opponent's next full turn** (they can try to break it by moving/capturing). If, when your next turn begins, the Harmony still exists (same set or **any valid set** of ≥3), **you win immediately**
|
||||
|
||||
> Implementation: On declare, snapshot the **set of piece IDs** and the **progression type + parameters** (e.g., `(AP, v=6,d=6)`). On the next time it becomes the declarer's turn, **re-validate** either the same set OR allow **any** new valid ≥3 set controlled by the declarer in enemy half (choose one policy now: we pick **any valid set** to reward dynamic play).
|
||||
**Procedure:**
|
||||
|
||||
1. On your turn, complete the arrangement (by moving one piece)
|
||||
2. **Announce** the proportion (e.g., "harmonic 6–8–12 on column D")
|
||||
3. Opponent verifies the numeric relation and board condition
|
||||
4. If valid, harmony is **pending**—opponent gets one turn to break it
|
||||
5. If still valid at start of your next turn, you **win**
|
||||
|
||||
> **Implementation:** On declare, snapshot the **set of piece IDs**, **proportion type**, and **parameters**. On the declarer's next turn start, **re-validate** either the same set OR allow **any** new valid harmony (we choose **any valid set** to reward dynamic play).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -48,13 +48,12 @@ export type HarmonyType = 'ARITH' | 'GEOM' | 'HARM'
|
|||
|
||||
export interface HarmonyDeclaration {
|
||||
by: Color
|
||||
pieceIds: string[] // ≥3
|
||||
pieceIds: string[] // exactly 3 for classical three-piece proportions
|
||||
type: HarmonyType
|
||||
params: {
|
||||
v?: string // store as strings for bigints
|
||||
d?: string // difference (ARITH)
|
||||
r?: string // ratio (GEOM)
|
||||
n?: string // harmonic parameter
|
||||
a?: string // first value in proportion (A-M-B structure)
|
||||
m?: string // middle value in proportion
|
||||
b?: string // last value in proportion
|
||||
}
|
||||
declaredAtPly: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import type { Color, HarmonyDeclaration, HarmonyType, Piece } from '../types'
|
||||
import { isInEnemyHalf } from '../types'
|
||||
import { isInEnemyHalf, parseSquare } from '../types'
|
||||
import { getEffectiveValue } from './pieceSetup'
|
||||
|
||||
/**
|
||||
* Harmony (progression) validator for Rithmomachia.
|
||||
* Detects arithmetic, geometric, and harmonic progressions.
|
||||
* Detects arithmetic, geometric, and harmonic proportions using three pieces.
|
||||
*
|
||||
* Updated to match classical Rithmomachia rules:
|
||||
* - Three pieces (A-M-B) where M is spatially in the middle
|
||||
* - Must be in a straight line (row, column, or diagonal)
|
||||
* - Uses three-piece proportion formulas (no division needed)
|
||||
*/
|
||||
|
||||
export interface HarmonyValidationResult {
|
||||
|
|
@ -14,166 +19,211 @@ export interface HarmonyValidationResult {
|
|||
reason?: string
|
||||
}
|
||||
|
||||
export type HarmonyLayoutMode = 'adjacent' | 'equalSpacing' | 'collinear'
|
||||
|
||||
/**
|
||||
* Check if values form an arithmetic progression.
|
||||
* Arithmetic: v, v+d, v+2d, ... with d > 0
|
||||
* Check if three squares are collinear (on same row, column, or diagonal)
|
||||
*/
|
||||
function isArithmeticProgression(values: number[]): HarmonyValidationResult {
|
||||
if (values.length < 2) {
|
||||
return { valid: false, reason: 'Need at least 2 values' }
|
||||
function areCollinear(sq1: string, sq2: string, sq3: string): boolean {
|
||||
const p1 = parseSquare(sq1)
|
||||
const p2 = parseSquare(sq2)
|
||||
const p3 = parseSquare(sq3)
|
||||
|
||||
if (!p1 || !p2 || !p3) return false
|
||||
|
||||
// Same rank (horizontal row)
|
||||
if (p1.rank === p2.rank && p2.rank === p3.rank) return true
|
||||
|
||||
// Same file (vertical column)
|
||||
if (p1.file === p2.file && p2.file === p3.file) return true
|
||||
|
||||
// Diagonal: check if slope is consistent
|
||||
const dx12 = p2.file - p1.file
|
||||
const dy12 = p2.rank - p1.rank
|
||||
const dx23 = p3.file - p2.file
|
||||
const dy23 = p3.rank - p2.rank
|
||||
|
||||
// Cross product should be zero for collinear points
|
||||
return dx12 * dy23 === dy12 * dx23
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the distance between two squares (Manhattan or diagonal)
|
||||
*/
|
||||
function getDistance(sq1: string, sq2: string): number {
|
||||
const p1 = parseSquare(sq1)
|
||||
const p2 = parseSquare(sq2)
|
||||
|
||||
if (!p1 || !p2) return Infinity
|
||||
|
||||
return Math.max(Math.abs(p2.file - p1.file), Math.abs(p2.rank - p1.rank))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which piece is spatially in the middle on a line
|
||||
* Returns the middle piece, or null if they're not properly ordered
|
||||
*/
|
||||
function findMiddlePiece(pieces: Piece[]): Piece | null {
|
||||
if (pieces.length !== 3) return null
|
||||
|
||||
const [p1, p2, p3] = pieces
|
||||
|
||||
// Check all permutations to find which one is in the middle
|
||||
const positions = [parseSquare(p1.square), parseSquare(p2.square), parseSquare(p3.square)]
|
||||
|
||||
if (!positions[0] || !positions[1] || !positions[2]) return null
|
||||
|
||||
// For each piece, check if it's between the other two
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const candidate = positions[i]
|
||||
const others = [positions[(i + 1) % 3], positions[(i + 2) % 3]]
|
||||
|
||||
// Check if candidate is between the other two on all axes
|
||||
const betweenX =
|
||||
(candidate.file >= others[0].file && candidate.file <= others[1].file) ||
|
||||
(candidate.file >= others[1].file && candidate.file <= others[0].file)
|
||||
|
||||
const betweenY =
|
||||
(candidate.rank >= others[0].rank && candidate.rank <= others[1].rank) ||
|
||||
(candidate.rank >= others[1].rank && candidate.rank <= others[0].rank)
|
||||
|
||||
if (betweenX && betweenY) {
|
||||
return pieces[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Sort values
|
||||
const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate common difference
|
||||
const d = sorted[1] - sorted[0]
|
||||
/**
|
||||
* Check if three pieces satisfy layout constraint
|
||||
*/
|
||||
function checkLayout(pieces: Piece[], mode: HarmonyLayoutMode): boolean {
|
||||
if (pieces.length !== 3) return false
|
||||
|
||||
if (d <= 0) {
|
||||
return { valid: false, reason: 'Arithmetic progression requires positive difference' }
|
||||
const [p1, p2, p3] = pieces
|
||||
const squares = [p1.square, p2.square, p3.square]
|
||||
|
||||
// All modes require collinearity
|
||||
if (!areCollinear(squares[0], squares[1], squares[2])) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check all consecutive differences
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
const actualDiff = sorted[i] - sorted[i - 1]
|
||||
if (actualDiff !== d) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Not arithmetic: diff ${sorted[i - 1]} → ${sorted[i]} is ${actualDiff}, expected ${d}`,
|
||||
}
|
||||
// Find which piece is in the middle
|
||||
const middle = findMiddlePiece(pieces)
|
||||
if (!middle) return false
|
||||
|
||||
const others = pieces.filter((p) => p !== middle)
|
||||
|
||||
if (mode === 'adjacent') {
|
||||
// All distances must be 1
|
||||
const d1 = getDistance(middle.square, others[0].square)
|
||||
const d2 = getDistance(middle.square, others[1].square)
|
||||
return d1 === 1 && d2 === 1
|
||||
}
|
||||
|
||||
if (mode === 'equalSpacing') {
|
||||
// Distances must be equal (and can be 1 or 2)
|
||||
const d1 = getDistance(middle.square, others[0].square)
|
||||
const d2 = getDistance(middle.square, others[1].square)
|
||||
return d1 === d2 && (d1 === 1 || d1 === 2)
|
||||
}
|
||||
|
||||
// mode === 'collinear': any spacing is OK (already checked collinearity)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if three values form an arithmetic proportion (A-M-B).
|
||||
* AP: 2M = A + B (middle is arithmetic mean)
|
||||
*/
|
||||
function isArithmeticProportion(a: number, m: number, b: number): HarmonyValidationResult {
|
||||
if (2 * m === a + b) {
|
||||
return {
|
||||
valid: true,
|
||||
type: 'ARITH',
|
||||
params: {
|
||||
a: a.toString(),
|
||||
m: m.toString(),
|
||||
b: b.toString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
type: 'ARITH',
|
||||
params: {
|
||||
v: sorted[0].toString(),
|
||||
d: d.toString(),
|
||||
},
|
||||
valid: false,
|
||||
reason: `Not arithmetic: 2·${m} ≠ ${a} + ${b} (${2 * m} ≠ ${a + b})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if values form a geometric progression.
|
||||
* Geometric: v, v·r, v·r², ... with integer r ≥ 2
|
||||
* Check if three values form a geometric proportion (A-M-B).
|
||||
* GP: M² = A · B (middle is geometric mean)
|
||||
*/
|
||||
function isGeometricProgression(values: number[]): HarmonyValidationResult {
|
||||
if (values.length < 2) {
|
||||
return { valid: false, reason: 'Need at least 2 values' }
|
||||
}
|
||||
|
||||
// Sort values
|
||||
const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
// Check for zero (can't have geometric with zero)
|
||||
if (sorted[0] === 0) {
|
||||
return { valid: false, reason: 'Geometric progression cannot start with 0' }
|
||||
}
|
||||
|
||||
// Calculate common ratio
|
||||
if (sorted[1] % sorted[0] !== 0) {
|
||||
return { valid: false, reason: 'Not geometric: ratio is not an integer' }
|
||||
}
|
||||
|
||||
const r = sorted[1] / sorted[0]
|
||||
|
||||
if (r < 2) {
|
||||
return { valid: false, reason: 'Geometric progression requires ratio ≥ 2' }
|
||||
}
|
||||
|
||||
// Check all consecutive ratios
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
if (sorted[i] % sorted[i - 1] !== 0) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Not geometric: ${sorted[i]} not divisible by ${sorted[i - 1]}`,
|
||||
}
|
||||
}
|
||||
const actualRatio = sorted[i] / sorted[i - 1]
|
||||
if (actualRatio !== r) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Not geometric: ratio ${sorted[i - 1]} → ${sorted[i]} is ${actualRatio}, expected ${r}`,
|
||||
}
|
||||
function isGeometricProportion(a: number, m: number, b: number): HarmonyValidationResult {
|
||||
if (m * m === a * b) {
|
||||
return {
|
||||
valid: true,
|
||||
type: 'GEOM',
|
||||
params: {
|
||||
a: a.toString(),
|
||||
m: m.toString(),
|
||||
b: b.toString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
type: 'GEOM',
|
||||
params: {
|
||||
v: sorted[0].toString(),
|
||||
r: r.toString(),
|
||||
},
|
||||
valid: false,
|
||||
reason: `Not geometric: ${m}² ≠ ${a} · ${b} (${m * m} ≠ ${a * b})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if values form a harmonic progression.
|
||||
* Harmonic: reciprocals form an arithmetic progression.
|
||||
* 1/v, 1/(v·n/(n-1)), 1/(v·n/(n-2)), ...
|
||||
* Check if three values form a harmonic proportion (A-M-B).
|
||||
* HP: 2AB = M(A + B) (middle is harmonic mean)
|
||||
* Equivalently: 1/A, 1/M, 1/B forms an arithmetic progression
|
||||
*/
|
||||
function isHarmonicProgression(values: number[]): HarmonyValidationResult {
|
||||
if (values.length < 3) {
|
||||
return { valid: false, reason: 'Harmonic progression requires at least 3 values' }
|
||||
}
|
||||
|
||||
// Sort values
|
||||
const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
// Check for zero
|
||||
if (sorted.some((v) => v === 0)) {
|
||||
return { valid: false, reason: 'Harmonic progression cannot contain 0' }
|
||||
}
|
||||
|
||||
// Calculate reciprocals as fractions (to avoid floating point)
|
||||
// We'll represent 1/v as a rational number and check if differences are equal
|
||||
|
||||
// For harmonic progression, we need:
|
||||
// 1/v1 - 1/v2 = 1/v2 - 1/v3 = ... = constant
|
||||
//
|
||||
// This means:
|
||||
// (v2 - v1) / (v1 * v2) = (v3 - v2) / (v2 * v3)
|
||||
//
|
||||
// Cross-multiply: (v2 - v1) * v2 * v3 = (v3 - v2) * v1 * v2
|
||||
// (v2 - v1) * v3 = (v3 - v2) * v1
|
||||
|
||||
for (let i = 1; i < sorted.length - 1; i++) {
|
||||
const v1 = sorted[i - 1]
|
||||
const v2 = sorted[i]
|
||||
const v3 = sorted[i + 1]
|
||||
|
||||
const leftSide = (v2 - v1) * v3
|
||||
const rightSide = (v3 - v2) * v1
|
||||
|
||||
if (leftSide !== rightSide) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Not harmonic: reciprocals do not form arithmetic progression',
|
||||
}
|
||||
function isHarmonicProportion(a: number, m: number, b: number): HarmonyValidationResult {
|
||||
if (a === 0 || b === 0 || m === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Harmonic proportion cannot contain 0',
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the harmonic parameter n
|
||||
// For the first three terms: 1/v, 1/(v·n/(n-1)), 1/(v·n/(n-2))
|
||||
// We can derive n from the relationship, but for simplicity we'll just validate
|
||||
// the harmonic property holds (which we already did above)
|
||||
if (2 * a * b === m * (a + b)) {
|
||||
return {
|
||||
valid: true,
|
||||
type: 'HARM',
|
||||
params: {
|
||||
a: a.toString(),
|
||||
m: m.toString(),
|
||||
b: b.toString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
type: 'HARM',
|
||||
params: {
|
||||
v: sorted[0].toString(),
|
||||
},
|
||||
valid: false,
|
||||
reason: `Not harmonic: 2·${a}·${b} ≠ ${m}·(${a}+${b}) (${2 * a * b} ≠ ${m * (a + b)})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a set of pieces forms a valid harmony.
|
||||
* Returns the first valid progression type found, or null.
|
||||
* Validate if three pieces form a valid harmony.
|
||||
* Returns the first valid proportion type found, or invalid result.
|
||||
*/
|
||||
export function validateHarmony(pieces: Piece[], color: Color): HarmonyValidationResult {
|
||||
export function validateHarmony(
|
||||
pieces: Piece[],
|
||||
color: Color,
|
||||
layoutMode: HarmonyLayoutMode = 'adjacent'
|
||||
): HarmonyValidationResult {
|
||||
// Check: exactly 3 pieces
|
||||
if (pieces.length !== 3) {
|
||||
return { valid: false, reason: 'Harmony requires exactly 3 pieces' }
|
||||
}
|
||||
|
||||
// Check: all pieces must be in enemy half
|
||||
const notInEnemyHalf = pieces.filter((p) => !isInEnemyHalf(p.square, color))
|
||||
if (notInEnemyHalf.length > 0) {
|
||||
|
|
@ -183,56 +233,65 @@ export function validateHarmony(pieces: Piece[], color: Color): HarmonyValidatio
|
|||
}
|
||||
}
|
||||
|
||||
// Check: need at least 3 pieces
|
||||
if (pieces.length < 3) {
|
||||
return { valid: false, reason: 'Harmony requires at least 3 pieces' }
|
||||
// Check: must satisfy layout constraint
|
||||
if (!checkLayout(pieces, layoutMode)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Pieces not in valid ${layoutMode} layout (must be collinear with correct spacing)`,
|
||||
}
|
||||
}
|
||||
|
||||
// Find middle piece
|
||||
const middle = findMiddlePiece(pieces)
|
||||
if (!middle) {
|
||||
return { valid: false, reason: 'Could not determine middle piece' }
|
||||
}
|
||||
|
||||
const others = pieces.filter((p) => p !== middle)
|
||||
|
||||
// Extract values (handling Pyramids)
|
||||
const values: number[] = []
|
||||
for (const piece of pieces) {
|
||||
const value = getEffectiveValue(piece)
|
||||
if (value === null) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Piece ${piece.id} has no effective value (Pyramid face not set?)`,
|
||||
}
|
||||
const getVal = (p: Piece) => {
|
||||
const val = getEffectiveValue(p)
|
||||
if (val === null) {
|
||||
throw new Error(`Piece ${p.id} has no effective value (Pyramid face not set?)`)
|
||||
}
|
||||
values.push(value)
|
||||
return val
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
const uniqueValues = new Set(values.map((v) => v.toString()))
|
||||
if (uniqueValues.size !== values.length) {
|
||||
return { valid: false, reason: 'Harmony cannot contain duplicate values' }
|
||||
}
|
||||
try {
|
||||
const m = getVal(middle)
|
||||
const a = getVal(others[0])
|
||||
const b = getVal(others[1])
|
||||
|
||||
// Try to detect progression type (in order: arithmetic, geometric, harmonic)
|
||||
const arithCheck = isArithmeticProgression(values)
|
||||
if (arithCheck.valid) {
|
||||
return arithCheck
|
||||
}
|
||||
// Check for duplicates
|
||||
if (a === m || m === b || a === b) {
|
||||
return { valid: false, reason: 'Harmony cannot contain duplicate values' }
|
||||
}
|
||||
|
||||
const geomCheck = isGeometricProgression(values)
|
||||
if (geomCheck.valid) {
|
||||
return geomCheck
|
||||
}
|
||||
// Try all three proportion types
|
||||
const apCheck = isArithmeticProportion(a, m, b)
|
||||
if (apCheck.valid) return apCheck
|
||||
|
||||
const harmCheck = isHarmonicProgression(values)
|
||||
if (harmCheck.valid) {
|
||||
return harmCheck
|
||||
}
|
||||
const gpCheck = isGeometricProportion(a, m, b)
|
||||
if (gpCheck.valid) return gpCheck
|
||||
|
||||
return { valid: false, reason: 'Values do not form any valid progression' }
|
||||
const hpCheck = isHarmonicProportion(a, m, b)
|
||||
if (hpCheck.valid) return hpCheck
|
||||
|
||||
return { valid: false, reason: 'Values do not form any valid proportion' }
|
||||
} catch (err) {
|
||||
return { valid: false, reason: (err as Error).message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all possible harmonies for a color from a set of pieces.
|
||||
* Returns an array of all valid 3+ piece combinations that form harmonies.
|
||||
* Returns an array of all valid 3-piece combinations that form harmonies.
|
||||
*/
|
||||
export function findPossibleHarmonies(
|
||||
pieces: Record<string, Piece>,
|
||||
color: Color
|
||||
color: Color,
|
||||
layoutMode: HarmonyLayoutMode = 'adjacent'
|
||||
): Array<{ pieceIds: string[]; validation: HarmonyValidationResult }> {
|
||||
const results: Array<{ pieceIds: string[]; validation: HarmonyValidationResult }> = []
|
||||
|
||||
|
|
@ -245,32 +304,18 @@ export function findPossibleHarmonies(
|
|||
return results
|
||||
}
|
||||
|
||||
// Generate all combinations of 3+ pieces
|
||||
// For simplicity, we'll check all subsets of size 3, 4, 5, etc.
|
||||
const maxSize = Math.min(candidatePieces.length, 8) // Limit to 8 for performance
|
||||
|
||||
function* combinations<T>(arr: T[], size: number): Generator<T[]> {
|
||||
if (size === 0) {
|
||||
yield []
|
||||
return
|
||||
}
|
||||
if (arr.length === 0) return
|
||||
|
||||
const [first, ...rest] = arr
|
||||
for (const combo of combinations(rest, size - 1)) {
|
||||
yield [first, ...combo]
|
||||
}
|
||||
yield* combinations(rest, size)
|
||||
}
|
||||
|
||||
for (let size = 3; size <= maxSize; size++) {
|
||||
for (const combo of combinations(candidatePieces, size)) {
|
||||
const validation = validateHarmony(combo, color)
|
||||
if (validation.valid) {
|
||||
results.push({
|
||||
pieceIds: combo.map((p) => p.id),
|
||||
validation,
|
||||
})
|
||||
// Generate all combinations of exactly 3 pieces
|
||||
for (let i = 0; i < candidatePieces.length; i++) {
|
||||
for (let j = i + 1; j < candidatePieces.length; j++) {
|
||||
for (let k = j + 1; k < candidatePieces.length; k++) {
|
||||
const combo = [candidatePieces[i], candidatePieces[j], candidatePieces[k]]
|
||||
const validation = validateHarmony(combo, color, layoutMode)
|
||||
if (validation.valid) {
|
||||
results.push({
|
||||
pieceIds: combo.map((p) => p.id),
|
||||
validation,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -284,22 +329,27 @@ export function findPossibleHarmonies(
|
|||
*/
|
||||
export function isHarmonyStillValid(
|
||||
pieces: Record<string, Piece>,
|
||||
harmony: HarmonyDeclaration
|
||||
harmony: HarmonyDeclaration,
|
||||
layoutMode: HarmonyLayoutMode = 'adjacent'
|
||||
): boolean {
|
||||
const relevantPieces = harmony.pieceIds.map((id) => pieces[id]).filter((p) => p && !p.captured)
|
||||
|
||||
if (relevantPieces.length < 3) {
|
||||
if (relevantPieces.length !== 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
const validation = validateHarmony(relevantPieces, harmony.by)
|
||||
const validation = validateHarmony(relevantPieces, harmony.by, layoutMode)
|
||||
return validation.valid
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ANY valid harmony exists for a color (for harmony persistence recheck).
|
||||
*/
|
||||
export function hasAnyValidHarmony(pieces: Record<string, Piece>, color: Color): boolean {
|
||||
const harmonies = findPossibleHarmonies(pieces, color)
|
||||
export function hasAnyValidHarmony(
|
||||
pieces: Record<string, Piece>,
|
||||
color: Color,
|
||||
layoutMode: HarmonyLayoutMode = 'adjacent'
|
||||
): boolean {
|
||||
const harmonies = findPossibleHarmonies(pieces, color, layoutMode)
|
||||
return harmonies.length > 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue