Numerous WebUI animations, improvements, AI fixes, opporitunity cost-based decision logic, etc.
This commit is contained in:
164
client/state-differ.js
Normal file
164
client/state-differ.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// StateDiffer - Detects what changed between game states
|
||||
// Generates movement instructions for the animation queue
|
||||
|
||||
class StateDiffer {
|
||||
constructor() {
|
||||
this.previousState = null;
|
||||
}
|
||||
|
||||
// Compare old and new state, return array of movements
|
||||
diff(oldState, newState) {
|
||||
const movements = [];
|
||||
|
||||
if (!oldState || !newState) {
|
||||
return movements;
|
||||
}
|
||||
|
||||
// Check for initial flip phase - still animate initial flips
|
||||
if (oldState.waiting_for_initial_flip && !newState.waiting_for_initial_flip) {
|
||||
// Initial flip just completed - detect which cards were flipped
|
||||
for (const newPlayer of newState.players) {
|
||||
const oldPlayer = oldState.players.find(p => p.id === newPlayer.id);
|
||||
if (oldPlayer) {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (!oldPlayer.cards[i].face_up && newPlayer.cards[i].face_up) {
|
||||
movements.push({
|
||||
type: 'flip',
|
||||
playerId: newPlayer.id,
|
||||
position: i,
|
||||
faceUp: true,
|
||||
card: newPlayer.cards[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return movements;
|
||||
}
|
||||
|
||||
// Still in initial flip selection - no animations
|
||||
if (newState.waiting_for_initial_flip) {
|
||||
return movements;
|
||||
}
|
||||
|
||||
// Check for turn change - the previous player just acted
|
||||
const previousPlayerId = oldState.current_player_id;
|
||||
const currentPlayerId = newState.current_player_id;
|
||||
const turnChanged = previousPlayerId !== currentPlayerId;
|
||||
|
||||
// Detect if a swap happened (discard changed AND a hand position changed)
|
||||
const newTop = newState.discard_top;
|
||||
const oldTop = oldState.discard_top;
|
||||
const discardChanged = newTop && (!oldTop ||
|
||||
oldTop.rank !== newTop.rank ||
|
||||
oldTop.suit !== newTop.suit);
|
||||
|
||||
// Find hand changes for the player who just played
|
||||
if (turnChanged && previousPlayerId) {
|
||||
const oldPlayer = oldState.players.find(p => p.id === previousPlayerId);
|
||||
const newPlayer = newState.players.find(p => p.id === previousPlayerId);
|
||||
|
||||
if (oldPlayer && newPlayer) {
|
||||
// First pass: detect swaps (card identity changed)
|
||||
const swappedPositions = new Set();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const oldCard = oldPlayer.cards[i];
|
||||
const newCard = newPlayer.cards[i];
|
||||
|
||||
// Card identity changed = swap happened at this position
|
||||
if (this.cardIdentityChanged(oldCard, newCard)) {
|
||||
swappedPositions.add(i);
|
||||
|
||||
// Use discard_top for the revealed card (more reliable for opponents)
|
||||
const revealedCard = newState.discard_top || { ...oldCard, face_up: true };
|
||||
|
||||
movements.push({
|
||||
type: 'swap',
|
||||
playerId: previousPlayerId,
|
||||
position: i,
|
||||
oldCard: revealedCard,
|
||||
newCard: newCard
|
||||
});
|
||||
break; // Only one swap per turn
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: detect flips (card went from face_down to face_up, not a swap)
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (swappedPositions.has(i)) continue; // Skip if already detected as swap
|
||||
|
||||
const oldCard = oldPlayer.cards[i];
|
||||
const newCard = newPlayer.cards[i];
|
||||
|
||||
if (this.cardWasFlipped(oldCard, newCard)) {
|
||||
movements.push({
|
||||
type: 'flip',
|
||||
playerId: previousPlayerId,
|
||||
position: i,
|
||||
faceUp: true,
|
||||
card: newCard
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect drawing (current player just drew)
|
||||
if (newState.has_drawn_card && !oldState.has_drawn_card) {
|
||||
// Discard pile decreased = drew from discard
|
||||
const drewFromDiscard = !newState.discard_top ||
|
||||
(oldState.discard_top &&
|
||||
(!newState.discard_top ||
|
||||
oldState.discard_top.rank !== newState.discard_top.rank ||
|
||||
oldState.discard_top.suit !== newState.discard_top.suit));
|
||||
|
||||
movements.push({
|
||||
type: drewFromDiscard ? 'draw-discard' : 'draw-deck',
|
||||
playerId: currentPlayerId
|
||||
});
|
||||
}
|
||||
|
||||
return movements;
|
||||
}
|
||||
|
||||
// Check if the card identity (rank+suit) changed between old and new
|
||||
// Returns true if definitely different cards, false if same or unknown
|
||||
cardIdentityChanged(oldCard, newCard) {
|
||||
// If both have rank/suit data, compare directly
|
||||
if (oldCard.rank && newCard.rank) {
|
||||
return oldCard.rank !== newCard.rank || oldCard.suit !== newCard.suit;
|
||||
}
|
||||
// Can't determine - assume same card (flip, not swap)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if a card was just flipped (same card, now face up)
|
||||
cardWasFlipped(oldCard, newCard) {
|
||||
return !oldCard.face_up && newCard.face_up;
|
||||
}
|
||||
|
||||
// Get a summary of movements for debugging
|
||||
summarize(movements) {
|
||||
return movements.map(m => {
|
||||
switch (m.type) {
|
||||
case 'flip':
|
||||
return `Flip: Player ${m.playerId} position ${m.position}`;
|
||||
case 'swap':
|
||||
return `Swap: Player ${m.playerId} position ${m.position}`;
|
||||
case 'discard':
|
||||
return `Discard: ${m.card.rank}${m.card.suit} from player ${m.fromPlayerId}`;
|
||||
case 'draw-deck':
|
||||
return `Draw from deck: Player ${m.playerId}`;
|
||||
case 'draw-discard':
|
||||
return `Draw from discard: Player ${m.playerId}`;
|
||||
default:
|
||||
return `Unknown: ${m.type}`;
|
||||
}
|
||||
}).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in app.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = StateDiffer;
|
||||
}
|
||||
Reference in New Issue
Block a user