stack-the-deck #1

Merged
gavin merged 10 commits from stack-the-deck into trunk 2025-06-13 07:38:52 -04:00
6 changed files with 79 additions and 9 deletions
Showing only changes of commit e75d9b41bc - Show all commits

View File

@@ -78,6 +78,15 @@ export default function GamePage() {
socket.emit('flip-card', flip); socket.emit('flip-card', flip);
}; };
const redraw = (cardIndex: number) => {
const redraw: ClientUpdate = {
gameID,
cardIndex,
};
socket.emit('redraw', redraw);
};
const handleSettings = (gameData: GameUpdate) => { const handleSettings = (gameData: GameUpdate) => {
socket.emit('settings', { gameID, gameData }); socket.emit('settings', { gameID, gameData });
}; };
@@ -114,6 +123,7 @@ export default function GamePage() {
position={layout[cardMap[index]]} position={layout[cardMap[index]]}
settings={settings} settings={settings}
flipAction={() => flipCard(cardMap[index])} flipAction={() => flipCard(cardMap[index])}
redrawAction={() => redraw(cardMap[index])}
/> />
)} )}
</div> </div>

View File

@@ -17,9 +17,17 @@ type CardProps = {
position: Layout; position: Layout;
settings: Settings; settings: Settings;
flipAction: () => void; flipAction: () => void;
redrawAction: () => void;
}; };
export default function Card({ dm, card, position, settings, flipAction }: CardProps) { export default function Card({
dm,
card,
position,
settings,
flipAction,
redrawAction,
}: CardProps) {
const [tooltip, setTooltip] = useState<React.ReactNode>(null); const [tooltip, setTooltip] = useState<React.ReactNode>(null);
const { aria, flipped } = card; const { aria, flipped } = card;
@@ -71,8 +79,8 @@ export default function Card({ dm, card, position, settings, flipAction }: CardP
/> />
{dm && !flipped && ( {dm && !flipped && (
<StackTheDeck <StackTheDeck
onRedo={() => console.log('Redo')} onRedraw={redrawAction}
onPick={() => console.log('Pick')} onSelect={() => console.log('Pick')}
onHover={setTooltip} onHover={setTooltip}
/> />
)} )}

View File

@@ -1,15 +1,15 @@
import { GalleryHorizontalEnd, RefreshCw } from 'lucide-react'; import { GalleryHorizontalEnd, RefreshCw } from 'lucide-react';
interface StackTheDeckProps { interface StackTheDeckProps {
onRedo: () => void; onRedraw: () => void;
onPick: () => void; onSelect: () => void;
onHover: (state: React.ReactNode) => void; onHover: (state: React.ReactNode) => void;
className?: string; className?: string;
} }
export default function StackTheDeck({ export default function StackTheDeck({
onRedo, onRedraw,
onPick, onSelect,
onHover, onHover,
className = '', className = '',
}: StackTheDeckProps) { }: StackTheDeckProps) {
@@ -28,7 +28,7 @@ export default function StackTheDeck({
onTouchStart={() => onHover(<p className="text-yellow-400">Redraw</p>)} onTouchStart={() => onHover(<p className="text-yellow-400">Redraw</p>)}
onTouchEnd={() => onHover(null)} onTouchEnd={() => onHover(null)}
className={`p-1 transition-all duration-250 text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] cursor-pointer`} className={`p-1 transition-all duration-250 text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] cursor-pointer`}
onClick={curryHandleClick(onRedo)} onClick={curryHandleClick(onRedraw)}
> >
<RefreshCw className="w-3 h-3" /> <RefreshCw className="w-3 h-3" />
</button> </button>
@@ -39,7 +39,7 @@ export default function StackTheDeck({
onTouchStart={() => onHover(<p className="text-yellow-400">Select</p>)} onTouchStart={() => onHover(<p className="text-yellow-400">Select</p>)}
onTouchEnd={() => onHover(null)} onTouchEnd={() => onHover(null)}
className={`p-1 transition-all duration-250 text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] cursor-pointer`} className={`p-1 transition-all duration-250 text-yellow-400 hover:text-yellow-300 hover:drop-shadow-[0_0_3px_#ffd700] cursor-pointer`}
onClick={curryHandleClick(onPick)} onClick={curryHandleClick(onSelect)}
> >
<GalleryHorizontalEnd className="w-3 h-3" /> <GalleryHorizontalEnd className="w-3 h-3" />
</button> </button>

View File

@@ -130,6 +130,19 @@ export default class GameStore {
return this.gameUpdate(game); return this.gameUpdate(game);
} }
redraw(gameID: string, cardIndex: number): GameUpdate {
const game = this.getGame(gameID);
const card = game.cards[cardIndex];
if (!card) throw new Error(`Card ${cardIndex} not found`);
game.cards[cardIndex] =
card.suit === 'High Deck' ? deck.drawHigh(game.cards) : deck.drawLow(game.cards);
game.lastUpdated = Date.now();
return this.gameUpdate(game);
}
updateSettings(gameID: string, settings: Settings) { updateSettings(gameID: string, settings: Settings) {
const game = this.getGame(gameID); const game = this.getGame(gameID);

View File

@@ -19,6 +19,30 @@ export default class TarokkaDeck {
); );
} }
drawLow(exclude: TarokkaGameCard[] = []): TarokkaGameCard {
const excludeIDs = exclude.map(({ id }) => id);
return {
...getRandomItems(
this.commonDeck.filter(({ id }) => !excludeIDs.includes(id)),
1,
)[0],
flipped: false,
};
}
drawHigh(exclude: TarokkaGameCard[] = []): TarokkaGameCard {
const excludeIDs = exclude.map(({ id }) => id);
return {
...getRandomItems(
this.highDeck.filter(({ id }) => !excludeIDs.includes(id)),
1,
)[0],
flipped: false,
};
}
getBack(): TarokkaCard { getBack(): TarokkaCard {
return this.backs[0]; return this.backs[0];
} }

View File

@@ -79,6 +79,21 @@ app.prepare().then(() => {
} }
}); });
socket.on('redraw', ({ gameID, cardIndex }: ClientUpdate) => {
try {
//console.log(Date.now(), 'Redraw', { gameID, cardIndex });
const gameUpdate = gameStore.redraw(gameID, cardIndex);
broadcast('game-update', gameUpdate);
} catch (e) {
const error = e instanceof Error ? e.message : e;
console.error(Date.now(), 'Error[redraw]', error);
socket.emit('redraw-error', error);
}
});
socket.on('settings', ({ gameID, gameData }: { gameID: string; gameData: GameUpdate }) => { socket.on('settings', ({ gameID, gameData }: { gameID: string; gameData: GameUpdate }) => {
try { try {
const gameUpdate = gameStore.updateSettings(gameID, gameData.settings); const gameUpdate = gameStore.updateSettings(gameID, gameData.settings);