Skip to content

Commit 753aa78

Browse files
committed
games: Support reserving cards from the deck in Splendor
1 parent af1d0e8 commit 753aa78

6 files changed

Lines changed: 155 additions & 61 deletions

File tree

src/ps/games/splendor/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export enum ACTIONS {
2121
export enum VIEW_ACTION_TYPE {
2222
NONE = 'none',
2323
CLICK_WILD = 'wild',
24+
CLICK_DECK = 'deck',
2425
CLICK_RESERVE = 'payback',
2526
CLICK_TOKENS = 'tokens',
2627
TOO_MANY_TOKENS = 'discard',

src/ps/games/splendor/index.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export { meta } from '@/ps/games/splendor/meta';
2525

2626
export class Splendor extends BaseGame<State> {
2727
log: Log[] = [];
28-
winCtx?: WinCtx | { type: EndType };
28+
declare winCtx?: WinCtx | { type: EndType };
2929

3030
constructor(ctx: BaseContext) {
3131
super(ctx);
@@ -214,6 +214,21 @@ export class Splendor extends BaseGame<State> {
214214
this.update(user.id);
215215
return;
216216
}
217+
case VIEW_ACTION_TYPE.CLICK_DECK: {
218+
if (this.state.actionState.action === VIEW_ACTION_TYPE.TOO_MANY_TOKENS)
219+
throw new ChatError('You need to discard tokens!' as ToTranslate);
220+
if (!['1', '2', '3'].includes(actionCtx)) throw new ChatError('Which tier did you click on?' as ToTranslate);
221+
const tier = +actionCtx as 1 | 2 | 3;
222+
if (this.state.board.cards[tier].deck.length === 0)
223+
throw new ChatError(`The deck for tier ${tier} cards is empty!` as ToTranslate);
224+
225+
const canReserve = this.canReserve(player);
226+
if (!canReserve) throw new ChatError('You cannot reserve more than 3 cards at a time.' as ToTranslate);
227+
228+
this.state.actionState = { action: VIEW_ACTION_TYPE.CLICK_DECK, tier };
229+
this.update(user.id);
230+
return;
231+
}
217232

218233
case VIEW_ACTION_TYPE.TOO_MANY_TOKENS: {
219234
if (this.state.actionState.action !== VIEW_ACTION_TYPE.TOO_MANY_TOKENS)
@@ -262,26 +277,47 @@ export class Splendor extends BaseGame<State> {
262277
case ACTIONS.RESERVE: {
263278
if (this.state.actionState.action === VIEW_ACTION_TYPE.TOO_MANY_TOKENS)
264279
throw new ChatError('You have too many tokens!' as ToTranslate);
265-
const getCard = this.findWildCard(actionCtx);
266-
if (!getCard.success) throw new ChatError(getCard.error);
267-
268280
if (!this.canReserve(player)) {
269281
throw new ChatError(
270282
('You cannot reserve a card.' +
271283
'You may only reserve a card if a Dragon token is available AND you have less than three cards currently reserved.') as ToTranslate
272284
);
273285
}
274-
const card = getCard.data;
275286

276-
playerData.reserved.push(card);
287+
const deckReserve = ['1', '2', '3'].includes(actionCtx) ? +actionCtx : null;
288+
let reservedId: string;
289+
if (deckReserve) {
290+
const tier = deckReserve as 1 | 2 | 3;
291+
if (this.state.board.cards[tier].deck.length === 0)
292+
throw new ChatError(`The deck for tier ${tier} cards is empty!` as ToTranslate);
277293

278-
const stage = this.state.board.cards[card.tier];
279-
stage.wild.remove(card);
280-
stage.wild.push(...stage.deck.splice(0, 1));
294+
const [card] = this.state.board.cards[tier].deck.splice(0, 1);
295+
playerData.reserved.push(card);
296+
297+
reservedId = card.id;
298+
} else {
299+
const getCard = this.findWildCard(actionCtx);
300+
if (!getCard.success) throw new ChatError(getCard.error);
301+
302+
const card = getCard.data;
303+
304+
playerData.reserved.push(card);
305+
306+
const stage = this.state.board.cards[card.tier];
307+
stage.wild.remove(card);
308+
stage.wild.push(...stage.deck.splice(0, 1));
309+
310+
reservedId = card.id;
311+
}
281312

282313
this.getTokens({ [TOKEN_TYPE.DRAGON]: 1 }, playerData);
283314

284-
logEntry = { turn: player.turn, time: new Date(), action: ACTIONS.RESERVE, ctx: { id: card.id } };
315+
logEntry = {
316+
turn: player.turn,
317+
time: new Date(),
318+
action: ACTIONS.RESERVE,
319+
ctx: { id: reservedId, deck: deckReserve },
320+
};
285321
break;
286322
}
287323

src/ps/games/splendor/logs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type Log = Satisfies<
1919
}
2020
| {
2121
action: ACTIONS.RESERVE;
22-
ctx: { id: string; trainers?: string[] };
22+
ctx: { id: string; deck: number | null; trainers?: string[] };
2323
}
2424
| {
2525
action: ACTIONS.DRAW;

src/ps/games/splendor/render.tsx

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function renderLog(logEntry: Log, { id, players, $T, renderCtx: { msg } }
5757
const card = metadata.pokemon[logEntry.ctx.id];
5858
return [
5959
<Wrapper>
60-
<Username name={playerName} clickable /> reserved {card.name}.
60+
<Username name={playerName} clickable /> reserved {logEntry.ctx.deck ? `a Tier ${logEntry.ctx.deck} card` : card.name}.
6161
</Wrapper>,
6262
opts,
6363
];
@@ -198,7 +198,7 @@ function CardWrapper({
198198
}): ReactElement {
199199
if (!onClick) return <div style={style}>{children}</div>;
200200
return (
201-
<Button value={onClick} style={style}>
201+
<Button value={onClick} style={{ cursor: 'pointer', ...style }}>
202202
{children}
203203
</Button>
204204
);
@@ -306,25 +306,34 @@ export function PokemonCard({
306306
);
307307
}
308308

309-
function FlippedCard({ data, count }: { data: Card; count: number }): ReactElement {
309+
function FlippedCard({
310+
data,
311+
count,
312+
onClick,
313+
}: {
314+
data: { tier: number };
315+
count: number | null;
316+
onClick?: string | undefined;
317+
}): ReactElement {
310318
return (
311-
<div style={getCardStyles(getArtUrl('other', `back-${data.tier}.png`))}>
312-
<div
313-
style={{
314-
position: 'relative',
315-
top: 96,
316-
background: '#1119',
317-
borderRadius: 12,
318-
color: 'white',
319-
textShadow: '0 0 2px #000',
320-
display: 'inline-block',
321-
padding: '0 8px 8px',
322-
fontSize: 72,
323-
}}
324-
>
325-
×<b>{count}</b>
326-
</div>
327-
</div>
319+
<CardWrapper style={getCardStyles(getArtUrl('other', `back-${data.tier}.png`))} onClick={onClick}>
320+
{count ? (
321+
<div
322+
style={{
323+
background: '#1119',
324+
borderRadius: 12,
325+
color: 'white',
326+
textShadow: '0 0 2px #000',
327+
display: 'inline-block',
328+
padding: '0 8px 8px',
329+
fontSize: 72,
330+
...(!onClick ? { position: 'relative', top: 96 } : {}),
331+
}}
332+
>
333+
×<b>{count}</b>
334+
</div>
335+
) : null}
336+
</CardWrapper>
328337
);
329338
}
330339

@@ -354,7 +363,7 @@ export function Stack({
354363
cards.map((card, index) =>
355364
hidden ? (
356365
index === 0 ? (
357-
<FlippedCard data={card} count={cards.length} />
366+
<FlippedCard data={card} count={cards.length} onClick={onClick} />
358367
) : null
359368
) : (
360369
<PokemonCard data={card} reserved={reserved} onClick={onClick} stackIndex={index} />
@@ -444,6 +453,34 @@ function WildCardInput({ action, onClick }: { action: ViewType; onClick: string
444453
);
445454
}
446455

456+
function DeckReserveInput({ action, onClick }: { action: ViewType; onClick: string }): ReactElement {
457+
if (!action.active || action.action !== VIEW_ACTION_TYPE.CLICK_DECK) return <></>;
458+
return (
459+
<div style={{ borderRadius: 12, padding: 12, background: '#1119' }}>
460+
<FlippedCard data={{ tier: action.tier }} count={null} />
461+
<div
462+
style={{
463+
display: 'inline-block',
464+
verticalAlign: 'top',
465+
border: '1px solid',
466+
borderRadius: 12,
467+
padding: 24,
468+
margin: 12,
469+
}}
470+
>
471+
<Button
472+
value={`${onClick} ! ${ACTIONS.RESERVE} ${action.tier}`}
473+
style={{
474+
zoom: '240%',
475+
}}
476+
>
477+
{'Reserve!' as ToTranslate}
478+
</Button>
479+
</div>
480+
</div>
481+
);
482+
}
483+
447484
function ReservedCardInput({ card, preset, onClick }: { preset: TokenCount; card: Card; onClick: string }): ReactElement {
448485
const typesToInclude = (Object.keys(card.cost) as TOKEN_TYPE[]).concat([TOKEN_TYPE.DRAGON]);
449486
const typesToShow = AllTokenTypes.filter(type => typesToInclude.includes(type));
@@ -496,22 +533,34 @@ export function BaseBoard({ board, view, onClick }: { board: Board; view: ViewTy
496533
)}
497534
</div>
498535
{view.active && view.action === VIEW_ACTION_TYPE.CLICK_WILD ? <WildCardInput action={view} onClick={onClick!} /> : null}
536+
{view.active && view.action === VIEW_ACTION_TYPE.CLICK_DECK ? <DeckReserveInput action={view} onClick={onClick!} /> : null}
499537
<div style={{ height: 48 }} />
500-
{[board.cards[3], board.cards[2], board.cards[1]].map(({ wild, deck }) => (
501-
<div style={{ whiteSpace: 'nowrap', overflow: 'auto' }}>
502-
{wild.map(card => (
503-
<PokemonCard
504-
data={card}
538+
{([3, 2, 1] as const).map(tier => {
539+
const { wild, deck } = board.cards[tier];
540+
return (
541+
<div style={{ whiteSpace: 'nowrap', overflow: 'auto' }}>
542+
{wild.map(card => (
543+
<PokemonCard
544+
data={card}
545+
onClick={
546+
onClick && !(view.active && view.action === VIEW_ACTION_TYPE.CLICK_WILD && card.id === view.id)
547+
? `${onClick} ! ${VIEW_ACTION_TYPE.CLICK_WILD}`
548+
: undefined
549+
}
550+
/>
551+
))}
552+
<Stack
553+
cards={deck}
554+
hidden
505555
onClick={
506-
onClick && !(view.active && view.action === VIEW_ACTION_TYPE.CLICK_WILD && card.id === view.id)
507-
? `${onClick} ! ${VIEW_ACTION_TYPE.CLICK_WILD}`
556+
onClick && !(view.active && view.action === VIEW_ACTION_TYPE.CLICK_DECK && view.tier === tier)
557+
? `${onClick} ! ${VIEW_ACTION_TYPE.CLICK_DECK} ${tier}`
508558
: undefined
509559
}
510560
/>
511-
))}
512-
<Stack cards={deck} hidden />
513-
</div>
514-
))}
561+
</div>
562+
);
563+
})}
515564
</>
516565
);
517566
}

src/ps/games/splendor/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type ActionState =
6161
| { canBuy: true; preset: TokenCount }
6262
| { canBuy: false; preset: null }
6363
))
64+
| { action: VIEW_ACTION_TYPE.CLICK_DECK; tier: 1 | 2 | 3 }
6465
| { action: VIEW_ACTION_TYPE.CLICK_RESERVE; id: string; preset: TokenCount | null }
6566
| { action: VIEW_ACTION_TYPE.TOO_MANY_TOKENS; discard: number };
6667

src/ps/games/test.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const test: () => Promise<string> = async () => {
1414
const { render } = await import('@/ps/games/splendor/render');
1515
const { default: metadata } = (await import('@/ps/games/splendor/metadata.json')) as unknown as { default: Metadata };
1616

17+
// @ts-ignore
1718
const MOCK_RENDER_CTX: RenderCtx = {
1819
id: '#TEMP',
1920
header: 'Your turn!',
@@ -48,24 +49,30 @@ export const test: () => Promise<string> = async () => {
4849
[TOKEN_TYPE.COLORLESS]: 2,
4950
},
5051
},
51-
view: {
52-
type: 'player',
53-
active: true,
54-
self: 'partman',
55-
action: VIEW_ACTION_TYPE.CLICK_WILD,
56-
id: 'klang',
57-
canBuy: true,
58-
preset: {
59-
[TOKEN_TYPE.GRASS]: 2,
60-
[TOKEN_TYPE.WATER]: 1,
61-
[TOKEN_TYPE.FIRE]: 0,
62-
[TOKEN_TYPE.COLORLESS]: 0,
63-
[TOKEN_TYPE.DARK]: 0,
64-
[TOKEN_TYPE.DRAGON]: 0,
65-
},
66-
canReserve: true,
67-
// preset: null,
68-
},
52+
view: 1
53+
? {
54+
type: 'player',
55+
active: false,
56+
self: 'partman',
57+
}
58+
: {
59+
type: 'player',
60+
active: true,
61+
self: 'partman',
62+
action: VIEW_ACTION_TYPE.CLICK_WILD,
63+
id: 'klang',
64+
canBuy: true,
65+
preset: {
66+
[TOKEN_TYPE.GRASS]: 2,
67+
[TOKEN_TYPE.WATER]: 1,
68+
[TOKEN_TYPE.FIRE]: 0,
69+
[TOKEN_TYPE.COLORLESS]: 0,
70+
[TOKEN_TYPE.DARK]: 0,
71+
[TOKEN_TYPE.DRAGON]: 0,
72+
},
73+
canReserve: true,
74+
// preset: null,
75+
},
6976
turns: ['partbot', 'partman'],
7077
players: {
7178
partbot: {

0 commit comments

Comments
 (0)