diff --git a/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx b/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx index 5d8e9942..36534811 100644 --- a/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx +++ b/apps/web/src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx @@ -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 = { - 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 = { - SUM: '+', - DIFF: '−', - PRODUCT: '×', - RATIO: '÷', - EQUAL: '=', - MULTIPLE: '×', - DIVISOR: '÷', - } - const operator = operatorMap[relation] || '?' + const color = getRelationColor(relation) + const operator = getRelationOperator(relation) return ( @@ -1594,38 +1575,11 @@ function NumberBondVisualization({ return () => clearTimeout(timer) }, [autoAnimate]) - // Color scheme based on relation type - const colorMap: Record = { - 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 = { - 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 = { - 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 = { - 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 ( diff --git a/apps/web/src/arcade-games/rithmomachia/constants/board.ts b/apps/web/src/arcade-games/rithmomachia/constants/board.ts new file mode 100644 index 00000000..22ff238a --- /dev/null +++ b/apps/web/src/arcade-games/rithmomachia/constants/board.ts @@ -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 diff --git a/apps/web/src/arcade-games/rithmomachia/constants/captureRelations.ts b/apps/web/src/arcade-games/rithmomachia/constants/captureRelations.ts new file mode 100644 index 00000000..b1166e3c --- /dev/null +++ b/apps/web/src/arcade-games/rithmomachia/constants/captureRelations.ts @@ -0,0 +1,56 @@ +import type { RelationKind } from '../types' + +/** + * Color scheme for each capture relation type + */ +export const RELATION_COLORS: Record = { + 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 = { + SUM: '+', + DIFF: '−', + PRODUCT: '×', + RATIO: '÷', + EQUAL: '=', + MULTIPLE: '×', + DIVISOR: '÷', +} + +/** + * Human-readable descriptions for relation tooltips + */ +export const RELATION_TOOLTIPS: Record = { + 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] || '?' +} diff --git a/apps/web/src/arcade-games/rithmomachia/utils/boardCoordinates.ts b/apps/web/src/arcade-games/rithmomachia/utils/boardCoordinates.ts new file mode 100644 index 00000000..072a9f5d --- /dev/null +++ b/apps/web/src/arcade-games/rithmomachia/utils/boardCoordinates.ts @@ -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, + } +}