Skip to content

Commit 0438756

Browse files
committed
fix(engine): active_suit matching in all card-matching builtins and discard pile display
- card_matches_top, has_playable_card, played_card_matches_top now check active_suit string variable when comparing suits (fixes Crazy Eights 8-play) - Fix suit casing in choose_suit actions: 'Hearts' → 'hearts' (match deck preset) - Add set_face_up(discard, 0, true) to play_card effects so host shows card face - Client HandViewer filters null cards from non-owner zones, emphasizes top card - Host GameTable shows last card (top) instead of first card (bottom) in discard - Add 3 integration tests for active_suit matching flow
1 parent 9cf7312 commit 0438756

5 files changed

Lines changed: 274 additions & 32 deletions

File tree

packages/client/src/components/HandViewer.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import React, { useCallback } from "react";
99
import type { CSSProperties } from "react";
10-
import type { PlayerView, CardInstanceId } from "@card-engine/shared";
10+
import type { Card, PlayerView, CardInstanceId } from "@card-engine/shared";
1111
import { CardMini } from "./CardMini.js";
1212

1313
interface HandViewerProps {
@@ -114,20 +114,31 @@ export function HandViewer({
114114
return (
115115
<div style={containerStyle}>
116116
{/* Other visible zones (discard, community) — shown at top for read-only context */}
117-
{otherZones.map(([name, zone]) => (
118-
<div key={name} style={zoneStyle}>
119-
<p style={zoneLabelStyle}>{formatZoneName(name)}</p>
120-
<div style={cardsRowStyle}>
121-
{zone.cards.map((card, index) => (
122-
<CardMini
123-
key={card?.id ?? `hidden-${name}-${index}`}
124-
card={card}
125-
emphasized={index === 0}
126-
/>
127-
))}
117+
{otherZones.map(([name, zone]) => {
118+
// For non-owner zones (e.g. discard pile), only show visible cards
119+
// to avoid rendering dozens of face-down placeholders
120+
const displayCards = zone.cards.filter(
121+
(card): card is Card => card !== null,
122+
);
123+
124+
return (
125+
<div key={name} style={zoneStyle}>
126+
<p style={zoneLabelStyle}>{formatZoneName(name)}</p>
127+
<div style={cardsRowStyle}>
128+
{displayCards.map((card, index) => (
129+
<CardMini
130+
key={card.id}
131+
card={card}
132+
emphasized={
133+
displayCards.length > 0 &&
134+
index === displayCards.length - 1
135+
}
136+
/>
137+
))}
138+
</div>
128139
</div>
129-
</div>
130-
))}
140+
);
141+
})}
131142

132143
{/* Player's personal zones — shown at bottom for thumb-friendly interaction */}
133144
{myZones.map(([name, zone]) => (

packages/host/src/screens/GameTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ function ZoneDisplay({
251251
<StackedDeck count={cards.length} />
252252
) : shouldShowTopOnly ? (
253253
<>
254-
<FlippableCardView card={cards[0]!} />
254+
<FlippableCardView card={cards[cards.length - 1]!} />
255255
<View style={styles.topCardMoreIndicator}>
256256
<Text style={styles.topCardMoreText}>
257257
+{cards.length - 1} more

packages/shared/src/engine/builtins.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,11 @@ const cardMatchesTopBuiltin: BuiltinFunction = (args, context) => {
560560
}
561561
const card = handZone.cards[cardIndex]!;
562562
const topCard = targetZone.cards[0]!;
563+
const activeSuit = context.state.stringVariables["active_suit"] ?? "";
564+
const matchSuit = activeSuit || topCard.suit;
563565
return {
564566
kind: "boolean",
565-
value: card.suit === topCard.suit || card.rank === topCard.rank,
567+
value: card.suit === matchSuit || card.rank === topCard.rank,
566568
};
567569
};
568570

@@ -580,8 +582,10 @@ const hasPlayableCardBuiltin: BuiltinFunction = (args, context) => {
580582
return { kind: "boolean", value: false };
581583
}
582584
const topCard = targetZone.cards[0]!;
585+
const activeSuit = context.state.stringVariables["active_suit"] ?? "";
586+
const matchSuit = activeSuit || topCard.suit;
583587
const found = handZone.cards.some(
584-
(card) => card.suit === topCard.suit || card.rank === topCard.rank,
588+
(card) => card.suit === matchSuit || card.rank === topCard.rank,
585589
);
586590
return { kind: "boolean", value: found };
587591
};
@@ -1327,9 +1331,11 @@ const playedCardMatchesTopBuiltin: BuiltinFunction = (args, context) => {
13271331

13281332
const card = handZone.cards[cardIndex]!;
13291333
const topCard = targetZone.cards[0]!;
1334+
const activeSuit = context.state.stringVariables["active_suit"] ?? "";
1335+
const matchSuit = activeSuit || topCard.suit;
13301336
return {
13311337
kind: "boolean",
1332-
value: card.suit === topCard.suit || card.rank === topCard.rank,
1338+
value: card.suit === matchSuit || card.rank === topCard.rank,
13331339
};
13341340
};
13351341

0 commit comments

Comments
 (0)