refactor(rithmomachia): extract constants and coordinate utilities (Phase 1)

Extract duplicated code into reusable modules to improve maintainability
and reduce code size in RithmomachiaGame.tsx.

**Constants extracted:**
- constants/captureRelations.ts: Relation colors, operators, tooltips
  - RELATION_COLORS map (was duplicated 3x)
  - RELATION_OPERATORS map (was duplicated 3x)
  - Helper functions: getRelationColor(), getRelationOperator()

- constants/board.ts: Board layout constants
  - Board dimensions, column/row labels
  - Default cell size, gap, padding values

**Utilities extracted:**
- utils/boardCoordinates.ts: Board position calculations
  - parseSquare(): Convert "A1" notation to file/rank indices
  - getSquarePosition(): Calculate pixel position from square notation
  - Replaces ~20 lines of duplicated coordinate calculation code

**Changes to RithmomachiaGame.tsx:**
- Removed 3 duplicate color/operator map definitions (~50 lines)
- Replaced inline coordinate calculations with getSquarePosition()
- Net reduction: ~65 lines of duplicated code

This is Phase 1 of the rithmomachia refactoring plan. Remaining phases:
- Phase 2: Extract capture UI components (~1,500 lines)
- Phase 3: Refactor board components (container/presentation split)
- Phase 4: Extract phase components (Setup, Playing, Results)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-01 18:37:32 -05:00
parent e9c320bb10
commit eace0ed529
4 changed files with 172 additions and 102 deletions

View File

@@ -16,6 +16,8 @@ import { useViewerId } from '@/hooks/useViewerId'
import { useAbacusSettings } from '@/hooks/useAbacusSettings'
import type { RosterWarning } from '@/components/nav/GameContextNav'
import { css } from '../../../../styled-system/css'
import { getRelationColor, getRelationOperator } from '../constants/captureRelations'
import { getSquarePosition } from '../utils/boardCoordinates'
import { useRithmomachia } from '../Provider'
import type { Piece, RelationKind, RithmomachiaConfig } from '../types'
import { validateMove } from '../utils/pathValidator'
@@ -1412,29 +1414,8 @@ function HelperSelectionOptions({
}
}
// Color scheme based on relation type
const colorMap: Record<RelationKind, string> = {
SUM: '#ef4444', // red
DIFF: '#f97316', // orange
PRODUCT: '#8b5cf6', // purple
RATIO: '#3b82f6', // blue
EQUAL: '#10b981', // green
MULTIPLE: '#eab308', // yellow
DIVISOR: '#06b6d4', // cyan
}
const color = colorMap[relation] || '#6b7280'
// Operator symbols
const operatorMap: Record<RelationKind, string> = {
SUM: '+',
DIFF: '',
PRODUCT: '×',
RATIO: '÷',
EQUAL: '=',
MULTIPLE: '×',
DIVISOR: '÷',
}
const operator = operatorMap[relation] || '?'
const color = getRelationColor(relation)
const operator = getRelationOperator(relation)
return (
<g>
@@ -1594,38 +1575,11 @@ function NumberBondVisualization({
return () => clearTimeout(timer)
}, [autoAnimate])
// Color scheme based on relation type
const colorMap: Record<RelationKind, string> = {
SUM: '#ef4444', // red
DIFF: '#f97316', // orange
PRODUCT: '#8b5cf6', // purple
RATIO: '#3b82f6', // blue
EQUAL: '#10b981', // green
MULTIPLE: '#eab308', // yellow
DIVISOR: '#06b6d4', // cyan
}
const color = colorMap[relation] || '#6b7280'
// Operation symbol based on relation
const operatorMap: Record<RelationKind, string> = {
SUM: '+',
DIFF: '',
PRODUCT: '×',
RATIO: '÷',
EQUAL: '=',
MULTIPLE: '×',
DIVISOR: '÷',
}
const operator = operatorMap[relation]
const color = getRelationColor(relation)
const operator = getRelationOperator(relation)
// Calculate actual board position for target
const targetFile = targetPiece.square.charCodeAt(0) - 65
const targetRank = Number.parseInt(targetPiece.square.slice(1), 10)
const targetRow = 8 - targetRank
const targetBoardPos = {
x: padding + targetFile * (cellSize + gap) + cellSize / 2,
y: padding + targetRow * (cellSize + gap) + cellSize / 2,
}
const targetBoardPos = getSquarePosition(targetPiece.square, { cellSize, gap, padding })
// Animation: Rotate and collapse from actual positions to target
const captureAnimation = useSpring({
@@ -2088,56 +2042,14 @@ function CaptureRelationOptions({
// Show only the current helper
const currentHelper = validHelpers[currentHelperIndex]
// Color scheme based on relation type
const colorMap: Record<RelationKind, string> = {
SUM: '#ef4444', // red
DIFF: '#f97316', // orange
PRODUCT: '#8b5cf6', // purple
RATIO: '#3b82f6', // blue
EQUAL: '#10b981', // green
MULTIPLE: '#eab308', // yellow
DIVISOR: '#06b6d4', // cyan
}
const color = colorMap[hoveredRelation] || '#6b7280'
const color = getRelationColor(hoveredRelation)
const operator = getRelationOperator(hoveredRelation)
// Operator symbols
const operatorMap: Record<RelationKind, string> = {
SUM: '+',
DIFF: '',
PRODUCT: '×',
RATIO: '÷',
EQUAL: '=',
MULTIPLE: '×',
DIVISOR: '÷',
}
const operator = operatorMap[hoveredRelation] || '?'
// Calculate mover position on board
const moverFile = moverPiece.square.charCodeAt(0) - 65
const moverRank = Number.parseInt(moverPiece.square.slice(1), 10)
const moverRow = 8 - moverRank
const moverPos = {
x: padding + moverFile * (cellSize + gap) + cellSize / 2,
y: padding + moverRow * (cellSize + gap) + cellSize / 2,
}
// Calculate target position on board
const targetFile = targetPiece.square.charCodeAt(0) - 65
const targetRank = Number.parseInt(targetPiece.square.slice(1), 10)
const targetRow = 8 - targetRank
const targetBoardPos = {
x: padding + targetFile * (cellSize + gap) + cellSize / 2,
y: padding + targetRow * (cellSize + gap) + cellSize / 2,
}
// Calculate current helper position on board
const helperFile = currentHelper.square.charCodeAt(0) - 65
const helperRank = Number.parseInt(currentHelper.square.slice(1), 10)
const helperRow = 8 - helperRank
const helperPos = {
x: padding + helperFile * (cellSize + gap) + cellSize / 2,
y: padding + helperRow * (cellSize + gap) + cellSize / 2,
}
// Calculate piece positions on board
const layout = { cellSize, gap, padding }
const moverPos = getSquarePosition(moverPiece.square, layout)
const targetBoardPos = getSquarePosition(targetPiece.square, layout)
const helperPos = getSquarePosition(currentHelper.square, layout)
return (
<g key={currentHelper.id}>

View File

@@ -0,0 +1,23 @@
/**
* Board layout constants
*/
export const BOARD_ROWS = 8
export const BOARD_COLUMNS = 8
/**
* Column labels (A-H)
*/
export const COLUMN_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] as const
/**
* Row labels (1-8)
*/
export const ROW_LABELS = [1, 2, 3, 4, 5, 6, 7, 8] as const
/**
* Default board dimensions (can be overridden)
*/
export const DEFAULT_CELL_SIZE = 64
export const DEFAULT_GAP = 4
export const DEFAULT_PADDING = 32

View File

@@ -0,0 +1,56 @@
import type { RelationKind } from '../types'
/**
* Color scheme for each capture relation type
*/
export const RELATION_COLORS: Record<RelationKind, string> = {
SUM: '#ef4444', // red
DIFF: '#f97316', // orange
PRODUCT: '#8b5cf6', // purple
RATIO: '#3b82f6', // blue
EQUAL: '#10b981', // green
MULTIPLE: '#eab308', // yellow
DIVISOR: '#06b6d4', // cyan
}
/**
* Mathematical operator symbols for each relation
*/
export const RELATION_OPERATORS: Record<RelationKind, string> = {
SUM: '+',
DIFF: '',
PRODUCT: '×',
RATIO: '÷',
EQUAL: '=',
MULTIPLE: '×',
DIVISOR: '÷',
}
/**
* Human-readable descriptions for relation tooltips
*/
export const RELATION_TOOLTIPS: Record<RelationKind, string> = {
EQUAL: 'Equality: a = b',
MULTIPLE: 'Multiple: b is a multiple of a',
DIVISOR: 'Divisor: a divides evenly into b',
SUM: 'Arithmetic Sum: a + c = b',
DIFF: 'Arithmetic Difference: b a = c',
PRODUCT: 'Geometric Product: a × c = b',
RATIO: 'Geometric Ratio: b ÷ a = c',
}
/**
* Get relation color with fallback
*/
export function getRelationColor(relation: RelationKind | null): string {
if (!relation) return '#6b7280' // gray fallback
return RELATION_COLORS[relation] || '#6b7280'
}
/**
* Get relation operator symbol with fallback
*/
export function getRelationOperator(relation: RelationKind | null): string {
if (!relation) return '?'
return RELATION_OPERATORS[relation] || '?'
}

View File

@@ -0,0 +1,79 @@
/**
* Board coordinate calculation utilities
*/
export interface BoardPosition {
x: number
y: number
}
export interface BoardLayout {
cellSize: number
gap: number
padding: number
}
/**
* Parse square notation (e.g., "A1", "H8") into file and rank indices
* @param square - Square notation (e.g., "A1")
* @returns Object with file (0-7) and rank (1-8)
*/
export function parseSquare(square: string): { file: number; rank: number } {
const file = square.charCodeAt(0) - 65 // 'A' = 0, 'B' = 1, etc.
const rank = Number.parseInt(square.slice(1), 10)
return { file, rank }
}
/**
* Convert file and rank to row index (0-7, top to bottom)
* @param rank - Rank number (1-8)
* @returns Row index (0-7)
*/
export function rankToRow(rank: number): number {
return 8 - rank
}
/**
* Get the center position of a square on the board
* @param square - Square notation (e.g., "A1")
* @param layout - Board layout configuration
* @returns Center position {x, y} in pixels
*/
export function getSquarePosition(square: string, layout: BoardLayout): BoardPosition {
const { file, rank } = parseSquare(square)
const row = rankToRow(rank)
return {
x: layout.padding + file * (layout.cellSize + layout.gap) + layout.cellSize / 2,
y: layout.padding + row * (layout.cellSize + layout.gap) + layout.cellSize / 2,
}
}
/**
* Get the top-left corner position of a square on the board
* @param square - Square notation (e.g., "A1")
* @param layout - Board layout configuration
* @returns Top-left position {x, y} in pixels
*/
export function getSquareCorner(square: string, layout: BoardLayout): BoardPosition {
const { file, rank } = parseSquare(square)
const row = rankToRow(rank)
return {
x: layout.padding + file * (layout.cellSize + layout.gap),
y: layout.padding + row * (layout.cellSize + layout.gap),
}
}
/**
* Get the total board dimensions
* @param layout - Board layout configuration
* @returns Total width and height in pixels
*/
export function getBoardDimensions(layout: BoardLayout): { width: number; height: number } {
const boardSize = 8 * layout.cellSize + 7 * layout.gap
return {
width: boardSize + 2 * layout.padding,
height: boardSize + 2 * layout.padding,
}
}