Files
soroban-abacus-flashcards/apps/web/scripts/parseBoardCSV.js
Thomas Hallock e2816ae88b feat(vision): improve remote camera calibration UX
- Add dual-stream calibration: phone sends both raw and cropped preview
  frames during calibration so users can see what practice will look like
- Add "Adjust" button to modify existing manual calibration without
  resetting to auto-detection first
- Hide calibration quad editor overlay when not in calibration mode
- Fix rotation buttons to update cropped preview immediately
- Add rate limiting (10fps) for cropped preview frames during calibration
- Fix multiple bugs preventing dual-stream mode from working:
  - Don't mark calibration as complete during preview mode
  - Don't stop detection loop when receiving preview calibration
  - Sync refs properly in frame mode change effects

Also includes accumulated formatting and cleanup changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:51:59 -06:00

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)
}