feat(rithmomachia): add helpful error messages for failed captures
When a capture attempt fails because no mathematical relation works, show a detailed error dialog explaining why each relation type can't be used. This helps players understand the mathematical requirements for captures. Changes to relationEngine.ts: - Updated all relation check functions to return explanation messages on failure - EQUAL: Shows actual values and why they don't match - MULTIPLE/DIVISOR: Shows attempted division result - SUM/DIFF/PRODUCT/RATIO: Shows what helper value is needed Changes to RithmomachiaGame.tsx: - Added CaptureErrorDialog component for displaying capture failures - Shows red error dialog when availableRelations.length === 0 - Lists all relations and specific explanations for why each failed - Includes Close button to dismiss the dialog Example error messages: - "9 ≠ 12 (values are not equal)" - "9 does not divide 12 evenly (12÷9=1.33...)" - "Helper 3 doesn't satisfy sum (need 3 but got 3)" - "SUM: No friendly piece can serve as helper" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1c665889b5
commit
b172440a41
|
|
@ -24,6 +24,147 @@ import {
|
|||
} from '../utils/relationEngine'
|
||||
import { PieceRenderer } from './PieceRenderer'
|
||||
|
||||
/**
|
||||
* Error dialog when no capture is possible
|
||||
*/
|
||||
function CaptureErrorDialog({
|
||||
targetPos,
|
||||
cellSize,
|
||||
moverPiece,
|
||||
targetPiece,
|
||||
onClose,
|
||||
closing,
|
||||
}: {
|
||||
targetPos: { x: number; y: number }
|
||||
cellSize: number
|
||||
moverPiece: Piece
|
||||
targetPiece: Piece
|
||||
onClose: () => void
|
||||
closing: boolean
|
||||
}) {
|
||||
const moverValue = getEffectiveValue(moverPiece)
|
||||
const targetValue = getEffectiveValue(targetPiece)
|
||||
|
||||
// Get explanations for why each relation failed
|
||||
const explanations: string[] = []
|
||||
if (
|
||||
moverValue !== undefined &&
|
||||
moverValue !== null &&
|
||||
targetValue !== undefined &&
|
||||
targetValue !== null
|
||||
) {
|
||||
explanations.push(checkEqual(moverValue, targetValue).explanation || '')
|
||||
explanations.push(checkMultiple(moverValue, targetValue).explanation || '')
|
||||
explanations.push(checkDivisor(moverValue, targetValue).explanation || '')
|
||||
explanations.push('SUM: No friendly piece can serve as helper')
|
||||
explanations.push('DIFF: No friendly piece can serve as helper')
|
||||
explanations.push('PRODUCT: No friendly piece can serve as helper')
|
||||
explanations.push('RATIO: No friendly piece can serve as helper')
|
||||
}
|
||||
|
||||
const entranceSpring = useSpring({
|
||||
from: { scale: 0, opacity: 0 },
|
||||
scale: closing ? 0 : 1,
|
||||
opacity: closing ? 0 : 1,
|
||||
config: { tension: 280, friction: 20 },
|
||||
})
|
||||
|
||||
return (
|
||||
<animated.g
|
||||
style={{
|
||||
opacity: entranceSpring.opacity,
|
||||
}}
|
||||
transform={to(
|
||||
[entranceSpring.scale],
|
||||
(s) => `translate(${targetPos.x}, ${targetPos.y}) scale(${s})`
|
||||
)}
|
||||
>
|
||||
<foreignObject
|
||||
x={-cellSize * 2.5}
|
||||
y={-cellSize * 2}
|
||||
width={cellSize * 5}
|
||||
height={cellSize * 4}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.95)',
|
||||
color: 'white',
|
||||
padding: `${cellSize * 0.3}px`,
|
||||
borderRadius: `${cellSize * 0.2}px`,
|
||||
fontSize: `${cellSize * 0.25}px`,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.5)',
|
||||
border: '3px solid rgba(255, 255, 255, 0.9)',
|
||||
maxHeight: `${cellSize * 3.5}px`,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: `${cellSize * 0.35}px`,
|
||||
marginBottom: `${cellSize * 0.2}px`,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
❌ Capture Not Possible
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: `${cellSize * 0.22}px`,
|
||||
marginBottom: `${cellSize * 0.15}px`,
|
||||
}}
|
||||
>
|
||||
No mathematical relation works:
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: `${cellSize * 0.18}px`,
|
||||
textAlign: 'left',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{explanations.filter(Boolean).map((exp, i) => (
|
||||
<div key={i} style={{ marginBottom: `${cellSize * 0.1}px` }}>
|
||||
• {exp}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}}
|
||||
style={{
|
||||
marginTop: `${cellSize * 0.2}px`,
|
||||
padding: `${cellSize * 0.15}px ${cellSize * 0.3}px`,
|
||||
borderRadius: `${cellSize * 0.15}px`,
|
||||
border: '2px solid white',
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
fontSize: `${cellSize * 0.22}px`,
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)'
|
||||
e.currentTarget.style.transform = 'scale(1.05)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)'
|
||||
e.currentTarget.style.transform = 'scale(1)'
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</animated.g>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Rithmomachia game component.
|
||||
* Orchestrates the game phases and UI.
|
||||
|
|
@ -2018,7 +2159,7 @@ function BoardDisplay() {
|
|||
)
|
||||
}
|
||||
|
||||
// Phase 1: Show relation options
|
||||
// Phase 1: Show relation options OR error if no valid relations
|
||||
if (captureDialogOpen && targetPos && !selectedRelation) {
|
||||
console.log('[Render] Showing CaptureRelationOptions')
|
||||
console.log('[Render] availableRelations:', availableRelations)
|
||||
|
|
@ -2036,6 +2177,20 @@ function BoardDisplay() {
|
|||
return null
|
||||
}
|
||||
|
||||
// Show error message if no valid relations
|
||||
if (availableRelations.length === 0) {
|
||||
return (
|
||||
<CaptureErrorDialog
|
||||
targetPos={targetPos}
|
||||
cellSize={cellSize}
|
||||
moverPiece={moverPiece}
|
||||
targetPiece={targetPiece}
|
||||
onClose={dismissDialog}
|
||||
closing={closingDialog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CaptureRelationOptions
|
||||
targetPos={targetPos}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ export function checkEqual(a: number, b: number): RelationCheckResult {
|
|||
explanation: `${a} == ${b}`,
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `${a} ≠ ${b} (values are not equal)`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,7 +34,7 @@ export function checkEqual(a: number, b: number): RelationCheckResult {
|
|||
* a % b == 0 (a is a multiple of b)
|
||||
*/
|
||||
export function checkMultiple(a: number, b: number): RelationCheckResult {
|
||||
if (b === 0) return { valid: false }
|
||||
if (b === 0) return { valid: false, explanation: 'Cannot check multiple with zero' }
|
||||
if (a % b === 0) {
|
||||
return {
|
||||
valid: true,
|
||||
|
|
@ -39,7 +42,10 @@ export function checkMultiple(a: number, b: number): RelationCheckResult {
|
|||
explanation: `${a} is a multiple of ${b} (${a}÷${b}=${a / b})`,
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `${a} is not a multiple of ${b} (${a}÷${b}=${(a / b).toFixed(2)}...)`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,7 +53,7 @@ export function checkMultiple(a: number, b: number): RelationCheckResult {
|
|||
* b % a == 0 (a is a divisor of b)
|
||||
*/
|
||||
export function checkDivisor(a: number, b: number): RelationCheckResult {
|
||||
if (a === 0) return { valid: false }
|
||||
if (a === 0) return { valid: false, explanation: 'Cannot divide by zero' }
|
||||
if (b % a === 0) {
|
||||
return {
|
||||
valid: true,
|
||||
|
|
@ -55,7 +61,10 @@ export function checkDivisor(a: number, b: number): RelationCheckResult {
|
|||
explanation: `${a} divides ${b} (${b}÷${a}=${b / a})`,
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `${a} does not divide ${b} evenly (${b}÷${a}=${(b / a).toFixed(2)}...)`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -77,7 +86,10 @@ export function checkSum(a: number, b: number, h: number): RelationCheckResult {
|
|||
explanation: `${b} + ${h} = ${a}`,
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `Helper ${h} doesn't satisfy sum (need ${Math.abs(b - a)} but got ${h})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,7 +117,10 @@ export function checkDiff(a: number, b: number, h: number): RelationCheckResult
|
|||
}
|
||||
}
|
||||
|
||||
return { valid: false }
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `Helper ${h} doesn't satisfy difference (|${a}-${h}|=${diff1}, |${b}-${h}|=${diff2})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,7 +142,12 @@ export function checkProduct(a: number, b: number, h: number): RelationCheckResu
|
|||
explanation: `${b} × ${h} = ${a}`,
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
const needed1 = a === 0 ? 'undefined' : (b / a).toFixed(2)
|
||||
const needed2 = b === 0 ? 'undefined' : (a / b).toFixed(2)
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `Helper ${h} doesn't satisfy product (need ${needed1} or ${needed2})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,7 +156,7 @@ export function checkProduct(a: number, b: number, h: number): RelationCheckResu
|
|||
* This is similar to PRODUCT but with explicit ratio semantics.
|
||||
*/
|
||||
export function checkRatio(a: number, b: number, r: number): RelationCheckResult {
|
||||
if (r === 0) return { valid: false }
|
||||
if (r === 0) return { valid: false, explanation: 'Cannot use zero as ratio helper' }
|
||||
|
||||
if (a * r === b) {
|
||||
return {
|
||||
|
|
@ -152,7 +172,12 @@ export function checkRatio(a: number, b: number, r: number): RelationCheckResult
|
|||
explanation: `${b} × ${r} = ${a}`,
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
const needed1 = a === 0 ? 'undefined' : (b / a).toFixed(2)
|
||||
const needed2 = b === 0 ? 'undefined' : (a / b).toFixed(2)
|
||||
return {
|
||||
valid: false,
|
||||
explanation: `Helper ${r} doesn't satisfy ratio (need ${needed1} or ${needed2})`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue