feat(14-1): manual turn change and input accumulation

- Turns are now only changed by a big, prominent button
- Players can make multiple inputs (balls left, fouls, re-rack) before changing turns
- Players can change turn with no input (0 balls potted, 0 foul)
- All actions are accumulated in local state and finalized on turn change
- No automatic turn changes remain

Refs #26
This commit is contained in:
Frank Schwenk
2025-06-24 11:01:28 +02:00
parent c6557dc050
commit e6a5dcebbe
2 changed files with 100 additions and 84 deletions

View File

@@ -334,4 +334,41 @@
color: #ff9800; color: #ff9800;
font-size: 1.15rem; font-size: 1.15rem;
border-bottom: 2px solid #ff9800; border-bottom: 2px solid #ff9800;
}
.turn-change-controls {
display: flex;
justify-content: center;
margin: 2.5rem 0 1.5rem 0;
}
.turn-change-btn {
background: #ff9800;
color: #222;
font-size: 2.2rem;
font-weight: 900;
padding: 2rem 4rem;
border: none;
border-radius: 18px;
box-shadow: 0 4px 32px rgba(255,152,0,0.18), 0 2px 8px rgba(0,0,0,0.12);
cursor: pointer;
transition: background 0.2s, color 0.2s, box-shadow 0.2s;
letter-spacing: 1px;
margin: 0 auto;
display: block;
}
.turn-change-btn:hover, .turn-change-btn:focus {
background: #ffa726;
color: #111;
box-shadow: 0 6px 40px rgba(255,152,0,0.28), 0 2px 8px rgba(0,0,0,0.18);
}
.pending-foul-info {
margin-left: 1.5rem;
font-size: 1.2rem;
color: #ffc107;
font-weight: 700;
}
.selected {
outline: 3px solid #ff9800 !important;
background: #fff3e0 !important;
color: #222 !important;
} }

View File

@@ -100,6 +100,10 @@ const GameLogTable = ({ log, players }) => {
}; };
const GameDetail141 = ({ game, onUpdate, onUndo, onForfeit, onBack }) => { const GameDetail141 = ({ game, onUpdate, onUndo, onForfeit, onBack }) => {
const [pendingBallsLeft, setPendingBallsLeft] = useState(null); // null means not set
const [pendingFouls, setPendingFouls] = useState(0);
const [pendingReRack, setPendingReRack] = useState(0);
const handleSelectStartingPlayer = (playerIndex) => { const handleSelectStartingPlayer = (playerIndex) => {
onUpdate({ onUpdate({
...game, ...game,
@@ -114,100 +118,68 @@ const GameDetail141 = ({ game, onUpdate, onUndo, onForfeit, onBack }) => {
const currentPlayer = game.players[game.currentPlayer]; const currentPlayer = game.players[game.currentPlayer];
const handleTurnEnd = (remainingBalls, foulPoints = 0) => { // Handlers now only update local state
if (remainingBalls > game.ballsOnTable) { const handleBallsLeft = (num) => {
console.error("Cannot leave more balls than are on the table."); setPendingBallsLeft(num);
return;
}
const ballsPotted = game.ballsOnTable - remainingBalls;
const newScore = currentPlayer.score + ballsPotted - foulPoints;
const updatedPlayers = game.players.map((p, index) =>
index === game.currentPlayer ? { ...p, score: newScore, consecutiveFouls: 0 } : p
);
const nextPlayer = (game.currentPlayer + 1) % game.players.length;
onUpdate({
...game,
players: updatedPlayers,
ballsOnTable: remainingBalls,
currentPlayer: nextPlayer,
log: [...(game.log || []), { type: 'turn', player: currentPlayer.name, ballsPotted, foulPoints, newScore, ballsOnTable: remainingBalls }],
});
}; };
const handleFoul = (foulType) => { const handleFoul = (foulType) => {
let foulPoints = 0; let foulPoints = 0;
let penalty = 0; let penalty = 0;
const newConsecutiveFouls = (currentPlayer.consecutiveFouls || 0) + 1; let newConsecutiveFouls = (currentPlayer.consecutiveFouls || 0) + 1;
if (foulType === 'standard') foulPoints = 1;
if (foulType === 'standard') { else if (foulType === 'break') foulPoints = 2;
foulPoints = 1; if (newConsecutiveFouls === 3) penalty = 15;
} else if (foulType === 'break') { setPendingFouls(pendingFouls + foulPoints + penalty);
foulPoints = 2;
}
if (newConsecutiveFouls === 3) {
penalty = 15;
}
const totalDeduction = foulPoints + penalty;
const newScore = currentPlayer.score - totalDeduction;
const updatedPlayers = game.players.map((p, index) => {
if (index === game.currentPlayer) {
return {
...p,
score: newScore,
consecutiveFouls: newConsecutiveFouls === 3 ? 0 : newConsecutiveFouls,
};
}
return p;
});
const nextPlayer = (game.currentPlayer + 1) % game.players.length;
onUpdate({
...game,
players: updatedPlayers,
currentPlayer: nextPlayer,
log: [
...(game.log || []),
{
type: 'foul',
player: currentPlayer.name,
foul: foulType,
foulPoints,
penalty,
totalDeduction,
newScore,
consecutiveFouls: newConsecutiveFouls
},
],
});
}; };
const handleReRack = (ballsToAdd) => { const handleReRack = (ballsToAdd) => {
const scoreIncrement = game.ballsOnTable + ballsToAdd - 15; setPendingReRack(pendingReRack + ballsToAdd);
};
// Turn change button handler
const handleTurnChange = () => {
// Calculate balls potted
const ballsOnTableBefore = game.ballsOnTable;
const ballsLeft = pendingBallsLeft !== null ? pendingBallsLeft : ballsOnTableBefore;
const ballsPotted = ballsOnTableBefore - ballsLeft;
// Calculate score
let newScore = currentPlayer.score + ballsPotted - pendingFouls;
// Handle re-rack scoring
if (pendingReRack > 0) {
const scoreIncrement = ballsLeft + pendingReRack - 15;
newScore += scoreIncrement;
}
// Update player state
const updatedPlayers = game.players.map((p, idx) => const updatedPlayers = game.players.map((p, idx) =>
idx === game.currentPlayer ? { ...p, score: p.score + scoreIncrement } : p idx === game.currentPlayer ? { ...p, score: newScore, consecutiveFouls: pendingFouls > 0 ? 0 : (p.consecutiveFouls || 0) } : p
); );
// Update balls on table
let newBallsOnTable = ballsLeft;
if (pendingReRack > 0) newBallsOnTable = 15;
// Log turn
const newLog = [...(game.log || []), {
type: 'turn',
player: currentPlayer.name,
ballsPotted,
foulPoints: pendingFouls,
newScore,
ballsOnTable: newBallsOnTable,
reRack: pendingReRack > 0 ? pendingReRack : undefined
}];
// Advance player
const nextPlayer = (game.currentPlayer + 1) % game.players.length;
onUpdate({ onUpdate({
...game, ...game,
players: updatedPlayers, players: updatedPlayers,
ballsOnTable: 15, ballsOnTable: newBallsOnTable,
log: [...(game.log || []), { currentPlayer: nextPlayer,
type: 'rerack', log: newLog,
player: currentPlayer.name,
ballsAdded: ballsToAdd,
ballsOnTableBefore: game.ballsOnTable,
ballsOnTable: 15,
scoreIncrement,
newScore: updatedPlayers[game.currentPlayer].score
}],
}); });
// Reset local state
setPendingBallsLeft(null);
setPendingFouls(0);
setPendingReRack(0);
}; };
return ( return (
@@ -243,9 +215,9 @@ const GameDetail141 = ({ game, onUpdate, onUndo, onForfeit, onBack }) => {
{Array.from({ length: 15 }, (_, i) => i + 1).map(num => ( {Array.from({ length: 15 }, (_, i) => i + 1).map(num => (
<button <button
key={num} key={num}
onClick={() => handleTurnEnd(num)} onClick={() => handleBallsLeft(num)}
disabled={num > game.ballsOnTable} disabled={num > game.ballsOnTable}
className={styles['potted-ball-btn']} className={styles['potted-ball-btn'] + (pendingBallsLeft === num ? ' ' + styles['selected'] : '')}
> >
{num} {num}
</button> </button>
@@ -254,13 +226,20 @@ const GameDetail141 = ({ game, onUpdate, onUndo, onForfeit, onBack }) => {
</div> </div>
<div className={styles['rerack-controls']}> <div className={styles['rerack-controls']}>
<button onClick={() => handleReRack(14)} className={styles['rerack-btn']}>+14 Re-Rack</button> <button onClick={() => handleReRack(14)} className={styles['rerack-btn'] + (pendingReRack === 14 ? ' ' + styles['selected'] : '')}>+14 Re-Rack</button>
<button onClick={() => handleReRack(15)} className={styles['rerack-btn']}>+15 Re-Rack</button> <button onClick={() => handleReRack(15)} className={styles['rerack-btn'] + (pendingReRack === 15 ? ' ' + styles['selected'] : '')}>+15 Re-Rack</button>
</div> </div>
<div className={styles['foul-controls']}> <div className={styles['foul-controls']}>
<button onClick={() => handleFoul('standard')} className={styles['foul-btn']}>Standard Foul (-1)</button> <button onClick={() => handleFoul('standard')} className={styles['foul-btn']}>Standard Foul (-1)</button>
<button onClick={() => handleFoul('break')} className={styles['foul-btn']}>Break Foul (-2)</button> <button onClick={() => handleFoul('break')} className={styles['foul-btn']}>Break Foul (-2)</button>
{pendingFouls > 0 && <span className={styles['pending-foul-info']}>Foulpunkte: {pendingFouls}</span>}
</div>
<div className={styles['turn-change-controls']}>
<button className={styles['turn-change-btn']} onClick={handleTurnChange}>
Aufnahme beenden / Turn wechseln
</button>
</div> </div>
<GameLogTable log={game.log} players={game.players} /> <GameLogTable log={game.log} players={game.players} />