Add comprehensive draggable playing guide modal with quick navigation: - Draggable modal on desktop, fixed on mobile - 5 quick-access sections: Overview, Pieces, Capture, Harmony, Victory - Integrated PieceRenderer SVGs for visual piece examples - Responsive layout for desktop and mobile - Modal persists during gameplay - "How to Play" button on setup page with updated description - "Guide" button in gameplay controls - Complete guide content from PLAYING_GUIDE.md Features: - Desktop: draggable modal that can be positioned anywhere - Mobile: full-screen responsive modal - Quick navigation tabs for easy reference - Visual piece examples with movement descriptions - Mathematical capture relations explained - Harmony progression examples with formulas - Strategy tips and victory conditions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
207 lines
5.8 KiB
JavaScript
207 lines
5.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Test script to parse the Rithmomachia board CSV and verify the layout.
|
|
*/
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const csvPath = path.join(
|
|
process.env.HOME,
|
|
'Downloads',
|
|
'rithmomachia board setup - Sheet1 (1).csv'
|
|
)
|
|
|
|
function parseCSV(csvContent) {
|
|
const lines = csvContent.trim().split('\n')
|
|
const pieces = []
|
|
|
|
// Process in triplets (color, shape, number)
|
|
for (let rankIndex = 0; rankIndex < 16; rankIndex++) {
|
|
const colorRowIndex = rankIndex * 3
|
|
const shapeRowIndex = rankIndex * 3 + 1
|
|
const numberRowIndex = rankIndex * 3 + 2
|
|
|
|
if (numberRowIndex >= lines.length) break
|
|
|
|
const colorRow = lines[colorRowIndex].split(',')
|
|
const shapeRow = lines[shapeRowIndex].split(',')
|
|
const numberRow = lines[numberRowIndex].split(',')
|
|
|
|
// Process each column (8 total)
|
|
for (let colIndex = 0; colIndex < 8; colIndex++) {
|
|
const color = colorRow[colIndex]?.trim()
|
|
const shape = shapeRow[colIndex]?.trim()
|
|
const numberStr = numberRow[colIndex]?.trim()
|
|
|
|
// Skip empty cells (but allow empty number for Pyramids)
|
|
if (!color || !shape) continue
|
|
|
|
// Map CSV position to game square
|
|
// CSV column → game row (1-8)
|
|
// CSV rank → game column (A-P)
|
|
const gameRow = colIndex + 1 // CSV col 0 → row 1, col 7 → row 8
|
|
const gameCol = String.fromCharCode(65 + rankIndex) // rank 0 → A, rank 15 → P
|
|
const square = `${gameCol}${gameRow}`
|
|
|
|
// Parse color
|
|
const pieceColor = color.toLowerCase() === 'black' ? 'B' : 'W'
|
|
|
|
// Parse type
|
|
let pieceType
|
|
const shapeLower = shape.toLowerCase()
|
|
if (shapeLower === 'circle') pieceType = 'C'
|
|
else if (shapeLower === 'triangle' || shapeLower === 'traingle')
|
|
pieceType = 'T' // Handle typo
|
|
else if (shapeLower === 'square') pieceType = 'S'
|
|
else if (shapeLower === 'pyramid') pieceType = 'P'
|
|
else {
|
|
console.warn(`Unknown shape "${shape}" at ${square}`)
|
|
continue
|
|
}
|
|
|
|
// Parse value/pyramid faces
|
|
if (pieceType === 'P') {
|
|
// Pyramid - number cell should be empty, use default faces
|
|
pieces.push({
|
|
color: pieceColor,
|
|
type: pieceType,
|
|
pyramidFaces: pieceColor === 'B' ? [36, 25, 16, 4] : [64, 49, 36, 25],
|
|
square,
|
|
})
|
|
} else {
|
|
// Regular piece needs a number
|
|
if (!numberStr) {
|
|
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`)
|
|
continue
|
|
}
|
|
|
|
const value = parseInt(numberStr, 10)
|
|
if (isNaN(value)) {
|
|
console.warn(`Invalid number "${numberStr}" at ${square}`)
|
|
continue
|
|
}
|
|
|
|
pieces.push({
|
|
color: pieceColor,
|
|
type: pieceType,
|
|
value,
|
|
square,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return pieces
|
|
}
|
|
|
|
function generateBoardDisplay(pieces) {
|
|
const lines = []
|
|
|
|
lines.push('\n=== Board Layout (Game Orientation) ===')
|
|
lines.push('BLACK (top)\n')
|
|
lines.push(
|
|
' A B C D E F G H I J K L M N O P'
|
|
)
|
|
|
|
for (let row = 8; row >= 1; row--) {
|
|
let line = `${row} `
|
|
for (let colCode = 65; colCode <= 80; colCode++) {
|
|
const col = String.fromCharCode(colCode)
|
|
const square = `${col}${row}`
|
|
const piece = pieces.find((p) => p.square === square)
|
|
|
|
if (piece) {
|
|
const val = piece.type === 'P' ? ' P' : piece.value.toString().padStart(3, ' ')
|
|
line += ` ${piece.color}${piece.type}${val} `
|
|
} else {
|
|
line += ' ---- '
|
|
}
|
|
}
|
|
lines.push(line)
|
|
}
|
|
|
|
lines.push('\nWHITE (bottom)\n')
|
|
|
|
return lines.join('\n')
|
|
}
|
|
|
|
function generateColumnSummaries(pieces) {
|
|
const lines = []
|
|
|
|
lines.push('\n=== Column-by-Column Summary ===\n')
|
|
|
|
for (let colCode = 65; colCode <= 80; colCode++) {
|
|
const col = String.fromCharCode(colCode)
|
|
const columnPieces = pieces
|
|
.filter((p) => p.square[0] === col)
|
|
.sort((a, b) => {
|
|
const rowA = parseInt(a.square.substring(1))
|
|
const rowB = parseInt(b.square.substring(1))
|
|
return rowA - rowB
|
|
})
|
|
|
|
if (columnPieces.length === 0) continue
|
|
|
|
const color = columnPieces[0].color === 'B' ? 'BLACK' : 'WHITE'
|
|
lines.push(`Column ${col} (${color}):`)
|
|
for (const piece of columnPieces) {
|
|
const val = piece.type === 'P' ? 'P[36,25,16,4]' : piece.value
|
|
lines.push(` ${piece.square}: ${piece.type}(${val})`)
|
|
}
|
|
lines.push('')
|
|
}
|
|
|
|
return lines.join('\n')
|
|
}
|
|
|
|
function countPieces(pieces) {
|
|
const blackPieces = pieces.filter((p) => p.color === 'B')
|
|
const whitePieces = pieces.filter((p) => p.color === 'W')
|
|
|
|
const countByType = (pieces) => {
|
|
const counts = { C: 0, T: 0, S: 0, P: 0 }
|
|
for (const p of pieces) counts[p.type]++
|
|
return counts
|
|
}
|
|
|
|
const blackCounts = countByType(blackPieces)
|
|
const whiteCounts = countByType(whitePieces)
|
|
|
|
console.log('\n=== Piece Counts ===')
|
|
console.log(
|
|
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`
|
|
)
|
|
console.log(
|
|
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`
|
|
)
|
|
}
|
|
|
|
// Main
|
|
try {
|
|
const csvContent = fs.readFileSync(csvPath, 'utf-8')
|
|
const pieces = parseCSV(csvContent)
|
|
|
|
console.log(`\nParsed ${pieces.length} pieces from CSV`)
|
|
console.log(generateBoardDisplay(pieces))
|
|
console.log(generateColumnSummaries(pieces))
|
|
countPieces(pieces)
|
|
|
|
// Save parsed data
|
|
const outputPath = path.join(
|
|
__dirname,
|
|
'..',
|
|
'src',
|
|
'arcade-games',
|
|
'rithmomachia',
|
|
'utils',
|
|
'parsedBoard.json'
|
|
)
|
|
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2))
|
|
console.log(`\n✅ Saved parsed board to: ${outputPath}`)
|
|
} catch (error) {
|
|
console.error('Error:', error.message)
|
|
process.exit(1)
|
|
}
|