Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,16 @@ def _update_game_state(game: Game, turn_finished: bool) -> None:

def _is_ai_turn(game: Game) -> bool:
state = game.game_state
return (
state.use_ai
and (
(state.resolving_one_off and state.current_action_player == 1)
or (state.resolving_four and state.current_action_player == 1)
or (state.resolving_seven and state.current_action_player == 1)
or (not state.resolving_one_off and state.turn == 1)
)
)
if not state.use_ai:
return False
if (
state.resolving_one_off
or state.resolving_three
or state.resolving_four
or state.resolving_seven
):
return state.current_action_player == 1
return state.turn == 1


async def _apply_action(session: GameSession, action: Action) -> None:
Expand Down
8 changes: 5 additions & 3 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ function App() {
() => legalActions.filter((action) => action.type === 'Discard From Hand'),
[legalActions],
)
const hasHumanActions = (actions: ActionView[]) =>
actions.some((action) => action.played_by === 0)
const sevenActions = useMemo(
() =>
legalActions.filter((action) =>
Expand All @@ -62,11 +64,11 @@ function App() {
!state?.resolving_four &&
!state?.resolving_seven
const discardModalActive =
Boolean(state?.resolving_three) && state?.current_action_player === 0
Boolean(state?.resolving_three) && hasHumanActions(discardActions)
const fourDiscardModalActive =
Boolean(state?.resolving_four) && state?.current_action_player === 0
Boolean(state?.resolving_four) && hasHumanActions(fourDiscardActions)
const sevenModalActive =
Boolean(state?.resolving_seven) && state?.current_action_player === 0
Boolean(state?.resolving_seven) && state?.pending_seven_player === 0
const sevenCards = state?.pending_seven_cards ?? []
const sevenActionsByCard = useMemo(() => {
const grouped = new Map<string, ActionView[]>()
Expand Down
290 changes: 290 additions & 0 deletions web/tests/e2e/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,296 @@ test('discard selection flow for three', async ({ page }) => {
await responsePromise
})

test('discard selection flow for four', async ({ page }) => {
await page.route('**/api/sessions', async (route) => {
const payload = {
session_id: 'four-discard-session',
state: {
hands: [
[
{
id: 'hand-1',
suit: 'DIAMONDS',
rank: 'TEN',
display: 'Ten of Diamonds',
played_by: 0,
purpose: null,
point_value: 10,
is_stolen: false,
attachments: [],
},
{
id: 'hand-2',
suit: 'SPADES',
rank: 'NINE',
display: 'Nine of Spades',
played_by: 0,
purpose: null,
point_value: 9,
is_stolen: false,
attachments: [],
},
],
[],
],
hand_counts: [2, 0],
fields: [[], []],
effective_fields: [[], []],
deck_count: 18,
discard_pile: [],
discard_count: 0,
scores: [0, 0],
targets: [21, 21],
turn: 1,
current_action_player: 1,
status: null,
resolving_two: false,
resolving_one_off: false,
resolving_three: false,
resolving_four: true,
pending_four_count: 2,
overall_turn: 3,
use_ai: true,
one_off_card_to_counter: null,
},
legal_actions: [
{
id: 0,
label: 'Discard Ten of Diamonds from hand',
type: 'Discard From Hand',
played_by: 0,
source: 'Hand',
requires_additional_input: false,
card: {
id: 'hand-1',
suit: 'DIAMONDS',
rank: 'TEN',
display: 'Ten of Diamonds',
played_by: 0,
purpose: null,
point_value: 10,
is_stolen: false,
attachments: [],
},
target: null,
},
{
id: 1,
label: 'Discard Nine of Spades from hand',
type: 'Discard From Hand',
played_by: 0,
source: 'Hand',
requires_additional_input: false,
card: {
id: 'hand-2',
suit: 'SPADES',
rank: 'NINE',
display: 'Nine of Spades',
played_by: 0,
purpose: null,
point_value: 9,
is_stolen: false,
attachments: [],
},
target: null,
},
],
state_version: 0,
ai_thinking: false,
}

await route.fulfill({ json: payload })
})

await page.route('**/api/sessions/four-discard-session/history', async (route) => {
await route.fulfill({ json: { entries: [], turn_counter: 3 } })
})

await page.route('**/api/sessions/four-discard-session', async (route) => {
const payload = {
session_id: 'four-discard-session',
state: {
hands: [
[
{
id: 'hand-1',
suit: 'DIAMONDS',
rank: 'TEN',
display: 'Ten of Diamonds',
played_by: 0,
purpose: null,
point_value: 10,
is_stolen: false,
attachments: [],
},
{
id: 'hand-2',
suit: 'SPADES',
rank: 'NINE',
display: 'Nine of Spades',
played_by: 0,
purpose: null,
point_value: 9,
is_stolen: false,
attachments: [],
},
],
[],
],
hand_counts: [2, 0],
fields: [[], []],
effective_fields: [[], []],
deck_count: 18,
discard_pile: [],
discard_count: 0,
scores: [0, 0],
targets: [21, 21],
turn: 1,
current_action_player: 1,
status: null,
resolving_two: false,
resolving_one_off: false,
resolving_three: false,
resolving_four: true,
pending_four_count: 2,
overall_turn: 3,
use_ai: true,
one_off_card_to_counter: null,
},
legal_actions: [
{
id: 0,
label: 'Discard Ten of Diamonds from hand',
type: 'Discard From Hand',
played_by: 0,
source: 'Hand',
requires_additional_input: false,
card: {
id: 'hand-1',
suit: 'DIAMONDS',
rank: 'TEN',
display: 'Ten of Diamonds',
played_by: 0,
purpose: null,
point_value: 10,
is_stolen: false,
attachments: [],
},
target: null,
},
{
id: 1,
label: 'Discard Nine of Spades from hand',
type: 'Discard From Hand',
played_by: 0,
source: 'Hand',
requires_additional_input: false,
card: {
id: 'hand-2',
suit: 'SPADES',
rank: 'NINE',
display: 'Nine of Spades',
played_by: 0,
purpose: null,
point_value: 9,
is_stolen: false,
attachments: [],
},
target: null,
},
],
state_version: 0,
ai_thinking: false,
}

await route.fulfill({ json: payload })
})

await page.route('**/api/sessions/four-discard-session/actions', async (route) => {
const payload = {
state: {
hands: [
[
{
id: 'hand-2',
suit: 'SPADES',
rank: 'NINE',
display: 'Nine of Spades',
played_by: 0,
purpose: null,
point_value: 9,
is_stolen: false,
attachments: [],
},
],
[],
],
hand_counts: [1, 0],
fields: [[], []],
effective_fields: [[], []],
deck_count: 18,
discard_pile: [
{
id: 'hand-1',
suit: 'DIAMONDS',
rank: 'TEN',
display: 'Ten of Diamonds',
played_by: null,
purpose: null,
point_value: 10,
is_stolen: false,
attachments: [],
},
],
discard_count: 1,
scores: [0, 0],
targets: [21, 21],
turn: 1,
current_action_player: 1,
status: null,
resolving_two: false,
resolving_one_off: false,
resolving_three: false,
resolving_four: false,
pending_four_count: 0,
overall_turn: 3,
use_ai: true,
one_off_card_to_counter: null,
},
legal_actions: [],
state_version: 1,
last_actions: [
{
id: -1,
label: 'Discard Ten of Diamonds from hand',
type: 'Discard From Hand',
played_by: 0,
source: 'Hand',
requires_additional_input: false,
card: null,
target: null,
},
],
}

await route.fulfill({ json: payload })
})

await page.goto('/')

await expect(page.getByText('Discard Cards')).toBeVisible()
await expect(
page.getByRole('button', { name: 'Ten of Diamonds' }),
).toBeVisible()

const responsePromise = page.waitForResponse(
'**/api/sessions/four-discard-session/actions',
)
await page.getByRole('button', { name: 'Ten of Diamonds' }).click()
await page.getByRole('button', { name: 'Discard' }).click()
await responsePromise
})

test('seven reveal modal flow', async ({ page }) => {
await page.route('**/api/sessions', async (route) => {
const payload = {
Expand Down
Loading