diff --git a/src/apps/rummy/components/__tests__/RummyTable.test.tsx b/src/apps/rummy/components/__tests__/RummyTable.test.tsx
index 72bb8e41..2b9f1c4c 100644
--- a/src/apps/rummy/components/__tests__/RummyTable.test.tsx
+++ b/src/apps/rummy/components/__tests__/RummyTable.test.tsx
@@ -20,6 +20,12 @@ const myHand = ['K♦', '7♥', '7♣', '8♥', '9♥', '2♠']
const view = (over: Partial = {}): RummyView => ({
gameId: 'M1',
phase: 'playing',
+ variant: 'basic',
+ dealNumber: 1,
+ standings: [
+ { playerId: 'alice', handsWon: 0 },
+ { playerId: 'bob', handsWon: 0 }
+ ],
players: [seat('alice', myHand), seat('bob')],
currentPlayerId: 'alice',
stage: 'play',
@@ -34,12 +40,29 @@ const view = (over: Partial = {}): RummyView => ({
...over
})
+// Between deals: the last deal's cards still on the felt, every hand
+// face up, and the next dealer choosing.
+const between = ({ dealer, dealNumber = 1, lastDeal }: { dealer: string; dealNumber?: number; lastDeal?: RummyView['lastDeal'] }): RummyView =>
+ view({
+ phase: 'choosing',
+ dealNumber,
+ currentPlayerId: undefined,
+ stage: undefined,
+ canDrawStock: false,
+ players: dealNumber === 0 ? [seat('alice', []), seat('bob', [])] : [seat('alice', myHand), seat('bob', ['A♠'])],
+ melds: dealNumber === 0 ? [] : view().melds,
+ choosing: { dealer, options: ['basic'] },
+ lastDeal
+ })
+
const table = (over: Partial = {}): RummyTableProps['table'] => ({
ended: null,
selected: [],
order: 'suit',
opening: false,
+ dealing: false,
startTable: vi.fn(),
+ chooseVariant: vi.fn(),
leaveTable: vi.fn(),
playAgain: vi.fn(),
drawStock: vi.fn(),
@@ -52,9 +75,9 @@ const table = (over: Partial = {}): RummyTableProps['t
...over
})
-const mountWith = (v: RummyView, over: Partial = {}, connected = true) => {
+const mountWith = (v: RummyView, over: Partial = {}, connected = true, away: string[] = []) => {
const t = table(over)
- const rendered = render()
+ const rendered = render()
return { ...rendered, t }
}
@@ -63,13 +86,15 @@ const myHandGroup = () => within(screen.getByRole('group', { name: 'Your hand' }
describe('RummyTable', () => {
beforeEach(() => cleanup())
- it('deals only once a second seat is in', () => {
- const solo = mountWith(view({ phase: 'waiting', players: [seat('alice', [])], currentPlayerId: undefined, stage: undefined }))
+ it('starts only once a second seat is in', () => {
+ const solo = mountWith(view({ phase: 'waiting', dealNumber: 0, players: [seat('alice', [])], currentPlayerId: undefined, stage: undefined }))
expect(screen.getByText('Waiting for a second seat.')).toBeDefined()
- expect(screen.getByRole('button', { name: 'Deal' })).toHaveProperty('disabled', true)
+ expect(screen.getByRole('button', { name: 'Start table' })).toHaveProperty('disabled', true)
cleanup()
- const pair = mountWith(view({ phase: 'waiting', players: [seat('alice', []), seat('bob', [])], currentPlayerId: undefined, stage: undefined }))
- fireEvent.click(screen.getByRole('button', { name: 'Deal' }))
+ const pair = mountWith(
+ view({ phase: 'waiting', dealNumber: 0, players: [seat('alice', []), seat('bob', [])], currentPlayerId: undefined, stage: undefined })
+ )
+ fireEvent.click(screen.getByRole('button', { name: 'Start table' }))
expect(pair.t.startTable).toHaveBeenCalledTimes(1)
expect(solo.t.startTable).not.toHaveBeenCalled()
})
@@ -239,27 +264,149 @@ describe('RummyTable', () => {
expect(screen.getByText('bob took 3♥')).toBeDefined()
})
- it('the ending names the winner and what everyone held, then gets out of the way', () => {
- const ended = { winner: 'bob', points: 43, scores: [{ playerId: 'alice', deadwood: 43 }, { playerId: 'bob', deadwood: 0 }] }
- const { t } = mountWith(
- view({ phase: 'ended', currentPlayerId: undefined, stage: undefined, players: [seat('alice', myHand), seat('bob', [])] }),
- { ended }
- )
+ it('the deal’s end names the winner and what everyone held; the dealer deals on from it', () => {
+ const lastDeal = { variant: 'basic', winner: 'bob', points: 43, scores: [{ playerId: 'alice', deadwood: 43 }, { playerId: 'bob', deadwood: 0 }] }
+ const { t } = mountWith(between({ lastDeal, dealer: 'alice' }))
const dialog = within(screen.getByRole('dialog'))
- expect(dialog.getByRole('heading', { name: 'bob wins' })).toBeDefined()
+ expect(dialog.getByRole('heading', { name: 'bob wins the hand' })).toBeDefined()
expect(dialog.getByText('bob went out and scores 43 points.')).toBeDefined()
expect(dialog.getByRole('row', { name: 'You 43 pts left' })).toBeDefined()
expect(dialog.getByRole('row', { name: 'bob wins 43 pts' })).toBeDefined()
+ expect(dialog.getByText('Your deal next.')).toBeDefined()
+ fireEvent.click(dialog.getByRole('button', { name: 'Deal Basic rummy' }))
+ expect(t.chooseVariant).toHaveBeenCalledWith('basic')
+ fireEvent.click(dialog.getByRole('button', { name: 'See the hands' }))
+ expect(screen.queryByRole('dialog')).toBeNull()
+ // The deal stays under the hand once the dialog is gone, and the last
+ // deal's cards stay on the felt to be read.
+ expect(screen.getByRole('button', { name: 'Deal Basic rummy' })).toBeDefined()
+ expect(screen.getByRole('group', { name: 'melds' })).toBeDefined()
+ expect(screen.getByText('bob went out and scores 43 points.')).toBeDefined()
+ })
+
+ it('at another seat, the deal’s end says who deals next and offers no deal', () => {
+ const lastDeal = { variant: 'basic', winner: 'alice', points: 12, scores: [] }
+ mountWith(between({ lastDeal, dealer: 'bob' }))
+ const dialog = within(screen.getByRole('dialog'))
+ expect(dialog.getByRole('heading', { name: 'You won the hand!' })).toBeDefined()
+ expect(dialog.getByText('bob deals next.')).toBeDefined()
+ expect(dialog.queryByRole('button', { name: /^Deal/ })).toBeNull()
+ fireEvent.click(dialog.getByRole('button', { name: 'See the hands' }))
+ expect(screen.getByText('Waiting for bob to deal.')).toBeDefined()
+ })
+
+ it('a deal broken up by a leave names nobody', () => {
+ mountWith(between({ lastDeal: { variant: 'basic', points: 0, scores: [] }, dealer: 'alice' }))
+ expect(within(screen.getByRole('dialog')).getByRole('heading', { name: 'The deal broke up' })).toBeDefined()
+ })
+
+ it('before the first deal, the dealer picks the game and nobody else can', () => {
+ const { t } = mountWith(between({ dealer: 'alice', dealNumber: 0 }))
+ expect(screen.queryByRole('dialog')).toBeNull()
+ expect(screen.getByText('Your deal: pick the game.')).toBeDefined()
+ expect(screen.queryByRole('group', { name: 'melds' })).toBeNull()
+ fireEvent.click(screen.getByRole('button', { name: 'Deal Basic rummy' }))
+ expect(t.chooseVariant).toHaveBeenCalledWith('basic')
+ cleanup()
+ mountWith(between({ dealer: 'bob', dealNumber: 0 }))
+ expect(screen.getByText('Waiting for bob to deal.')).toBeDefined()
+ expect(screen.queryByRole('button', { name: /^Deal/ })).toBeNull()
+ })
+
+ it('before any deal, a seat is a name: no card counts, no points, no sorting', () => {
+ mountWith(between({ dealer: 'alice', dealNumber: 0 }))
+ expect(screen.queryByText(/in hand/)).toBeNull()
+ expect(screen.queryByRole('group', { name: 'Sort your hand' })).toBeNull()
+ expect(screen.queryByText(/won/)).toBeNull()
+ })
+
+ it('a deal asked for is not asked twice', () => {
+ mountWith(between({ dealer: 'alice', dealNumber: 0 }), { dealing: true })
+ expect(screen.getByRole('button', { name: 'Dealing…' })).toHaveProperty('disabled', true)
+ })
+
+ it('between deals every hand is face up, so the felt marks the showdown', () => {
+ const { container } = mountWith(between({ dealer: 'bob', lastDeal: { variant: 'basic', winner: 'bob', points: 3, scores: [] } }))
+ expect(container.querySelector('[data-showdown]')).not.toBeNull()
+ cleanup()
+ const live = mountWith(view())
+ expect(live.container.querySelector('[data-showdown]')).toBeNull()
+ })
+
+ it('the deal’s result is said once, by the sheet, and describes it', () => {
+ const lastDeal = { variant: 'basic', winner: 'bob', points: 3, scores: [] }
+ mountWith(between({ dealer: 'bob', lastDeal }))
+ const dialog = screen.getByRole('dialog')
+ expect(screen.getAllByText('bob went out and scores 3 points.')).toHaveLength(1)
+ const described = (dialog.getAttribute('aria-describedby') ?? '').split(' ').map(id => document.getElementById(id)?.textContent)
+ expect(described).toEqual(['bob went out and scores 3 points.', 'bob deals next.'])
+ })
+
+ it('the next deal arriving puts focus on the hand, from the sheet or from under it', () => {
+ const lastDeal = { variant: 'basic', winner: 'bob', points: 3, scores: [] }
+ const { rerender, t } = mountWith(between({ dealer: 'bob', lastDeal }))
+ expect(document.activeElement?.textContent).toBe('See the hands')
+ rerender()
+ expect(document.activeElement).toBe(screen.getByRole('group', { name: 'Your hand' }))
+ })
+
+ it('the dealer going away while the sheet is up puts focus on the deal it now offers', () => {
+ const lastDeal = { variant: 'basic', winner: 'bob', points: 3, scores: [] }
+ const v = between({ dealer: 'bob', lastDeal })
+ const { rerender, t } = mountWith(v)
+ expect(document.activeElement?.textContent).toBe('See the hands')
+ rerender()
+ expect(document.activeElement?.textContent).toBe('Deal Basic rummy')
+ })
+
+ it('face up between deals, a seat shows its cards and points, not a count', () => {
+ mountWith(between({ dealer: 'alice', lastDeal: { variant: 'basic', winner: 'alice', points: 1, scores: [] } }))
+ const bob = screen.getByRole('region', { name: /^bob/ })
+ expect(bob.textContent).not.toContain('in hand')
+ expect(bob.textContent).toContain('1 pts left')
+ })
+
+ it('a dealer coming back while the sheet is up keeps focus in the sheet', () => {
+ const lastDeal = { variant: 'basic', winner: 'bob', points: 3, scores: [] }
+ const v = between({ dealer: 'bob', lastDeal })
+ const { rerender, t } = mountWith(v, {}, true, ['bob'])
+ expect(document.activeElement?.textContent).toBe('Deal Basic rummy')
+ rerender()
+ expect(screen.getByRole('dialog').contains(document.activeElement)).toBe(true)
+ })
+
+ it('a dealer the room shows away lets anyone deal', () => {
+ const { t } = mountWith(between({ dealer: 'bob', dealNumber: 0 }), {}, true, ['bob'])
+ expect(screen.getByText('bob is away: you can deal.')).toBeDefined()
+ fireEvent.click(screen.getByRole('button', { name: 'Deal Basic rummy' }))
+ expect(t.chooseVariant).toHaveBeenCalledWith('basic')
+ })
+
+ it('says which deal is on and what it plays, and the hands each seat has won', () => {
+ mountWith(
+ view({
+ dealNumber: 3,
+ standings: [
+ { playerId: 'alice', handsWon: 2 },
+ { playerId: 'bob', handsWon: 0 }
+ ]
+ })
+ )
+ expect(screen.getByText('Deal 3 · Basic rummy')).toBeDefined()
+ expect(screen.getByRole('region', { name: /^alice \(you\)/ }).textContent).toContain('2 won')
+ expect(screen.getByRole('region', { name: /^bob/ }).textContent).toContain('0 won')
+ })
+
+ it('the table’s end is the hands each seat won, then another table or the room', () => {
+ const ended = { standings: [{ playerId: 'alice', handsWon: 2 }], dealsPlayed: 3 }
+ const { t } = mountWith(view({ phase: 'ended', currentPlayerId: undefined, stage: undefined, players: [seat('alice', myHand)] }), { ended })
+ const dialog = within(screen.getByRole('dialog'))
+ expect(dialog.getByRole('heading', { name: 'The table closed' })).toBeDefined()
+ expect(dialog.getByText('You won 2 of 3 hands.')).toBeDefined()
fireEvent.click(dialog.getByRole('button', { name: 'Play again' }))
expect(t.playAgain).toHaveBeenCalledTimes(1)
fireEvent.click(dialog.getByRole('button', { name: 'See the final hands' }))
expect(screen.queryByRole('dialog')).toBeNull()
- // The ways on stay under the hand once the dialog is gone.
expect(screen.getByRole('button', { name: 'Back to the room' })).toBeDefined()
})
-
- it('a table that broke up names nobody', () => {
- mountWith(view({ phase: 'ended', currentPlayerId: undefined, stage: undefined }), { ended: { points: 0, scores: [] } })
- expect(within(screen.getByRole('dialog')).getByRole('heading', { name: 'The table broke up' })).toBeDefined()
- })
})
diff --git a/src/apps/rummy/rules.ts b/src/apps/rummy/rules.ts
index 85dc503d..1e6c6b5b 100644
--- a/src/apps/rummy/rules.ts
+++ b/src/apps/rummy/rules.ts
@@ -4,7 +4,7 @@
// only shape the offer, so they follow its rules to the letter — an offer
// the engine refuses is a button that does nothing.
-import type { Card, RummyGameEnded, RummyLastMove, RummyMeld, RummyPlayer, RummyView } from './wire'
+import type { Card, RummyDealResult, RummyGameEnded, RummyLastMove, RummyMeld, RummyPlayer, RummyView } from './wire'
import { face } from '@/apps/castle/rules'
export { face, isRed, enteredSince } from '@/apps/castle/rules'
@@ -98,17 +98,42 @@ export function describeLastMove(move: RummyLastMove, viewer: string): string {
const points = (n: number) => `${n} point${n === 1 ? '' : 's'}`
-// How a finished game reads from one chair.
-export function describeEnding(ended: RummyGameEnded, viewer: string): string {
- if (ended.winner === undefined) return 'Nobody went out.'
- return ended.winner === viewer
- ? `You went out and score ${points(ended.points)}.`
- : `${ended.winner} went out and scores ${points(ended.points)}.`
+// How a deal's end reads from one chair.
+export function describeEnding(deal: RummyDealResult, viewer: string): string {
+ if (deal.winner === undefined) return 'Nobody went out.'
+ return deal.winner === viewer
+ ? `You went out and score ${points(deal.points)}.`
+ : `${deal.winner} went out and scores ${points(deal.points)}.`
}
-export function headlineOf(ended: RummyGameEnded, viewer: string): string {
- if (ended.winner === undefined) return 'The table broke up'
- return ended.winner === viewer ? 'You won!' : `${ended.winner} wins`
+export function headlineOf(deal: RummyDealResult, viewer: string): string {
+ if (deal.winner === undefined) return 'The deal broke up'
+ return deal.winner === viewer ? 'You won the hand!' : `${deal.winner} wins the hand`
+}
+
+const hands = (n: number) => `${n} hand${n === 1 ? '' : 's'}`
+
+// How the table's end reads from one chair: the hands it won, or, to a
+// chair that left before the end, what was played.
+export function describeTableEnd(ended: RummyGameEnded, viewer: string): string {
+ const mine = ended.standings.find(standing => standing.playerId === viewer)
+ if (mine !== undefined) return `You won ${mine.handsWon} of ${hands(ended.dealsPlayed)}.`
+ return ended.dealsPlayed === 0 ? 'No hands played.' : `${hands(ended.dealsPlayed)} played.`
+}
+
+// Between deals the dealer deals; a dealer the room shows as away lets
+// anyone. The hub decides the same way from the same room.
+export function canDeal(view: RummyView, viewer: string, away: string[]): boolean {
+ if (view.phase !== 'choosing' || view.choosing === undefined) return false
+ return view.choosing.dealer === viewer || away.includes(view.choosing.dealer)
+}
+
+const VARIANT_LABELS: Record = { basic: 'Basic rummy' }
+
+// A variant as the table names it; one this build does not know yet reads
+// as the hub spelled it.
+export function variantLabel(variant: string): string {
+ return VARIANT_LABELS[variant] ?? variant
}
export function seatOf(view: RummyView, playerId: string): RummyPlayer | undefined {
diff --git a/src/apps/rummy/wire.ts b/src/apps/rummy/wire.ts
index dfe2c8a0..1e11e2bc 100644
--- a/src/apps/rummy/wire.ts
+++ b/src/apps/rummy/wire.ts
@@ -2,12 +2,16 @@
// model/rummy.smithy (#245). The view is the UI's model as well as the
// wire's: the hub already redacts per viewer, so there is nothing to
// translate.
+//
+// Every table is dealer's choice (MoonBase#1609): started, it waits
+// between deals on its dealer's pick; each deal's end passes the deal on;
+// it ends only when fewer than two seats are left.
import type { Card } from '@/apps/castle/wire'
export type { Card }
-export type RummyPhase = 'waiting' | 'playing' | 'ended'
+export type RummyPhase = 'waiting' | 'choosing' | 'playing' | 'ended'
// Where the seat on turn is: about to draw, or holding the drawn card
// with melds, lay-offs and the discard to come.
@@ -17,7 +21,7 @@ export interface RummyPlayer {
playerId: string
handCount: number
// Faces only for the viewer's own seat, in the order they arrived (a
- // drawn card last), and for every seat once the game ends.
+ // drawn card last), and for every seat once the deal ends.
hand: Card[]
}
@@ -40,9 +44,45 @@ export interface RummyLastMove {
meldIndex?: number
}
+export interface RummyStanding {
+ playerId: string
+ handsWon: number
+}
+
+export interface RummyScore {
+ playerId: string
+ deadwood: number
+}
+
+// A deal's result: the seat that went out and what it scored (everyone
+// else's cards left in hand), or no winner and no points for a deal
+// broken up by a leave.
+export interface RummyDealResult {
+ variant: string
+ winner?: string
+ points: number
+ scores: RummyScore[]
+}
+
+// Between deals: who deals next and what they may deal. A dealer the
+// room shows as not connected lets any seat deal.
+export interface RummyChoosing {
+ dealer: string
+ options: string[]
+}
+
export interface RummyView {
gameId: string
phase: RummyPhase
+ // The deal in play's variant, or the last one's.
+ variant?: string
+ // Deals dealt so far.
+ dealNumber: number
+ // Hands won, seat by seat.
+ standings: RummyStanding[]
+ choosing?: RummyChoosing
+ // The last deal's result, between deals and once the table ends.
+ lastDeal?: RummyDealResult
players: RummyPlayer[]
currentPlayerId?: string
stage?: RummyStage
@@ -59,17 +99,11 @@ export interface RummyView {
lastMove?: RummyLastMove
}
-export interface RummyScore {
- playerId: string
- deadwood: number
-}
-
-// The seat that went out and what it scored (everyone else's cards left
-// in hand), or no winner and no points for a table that broke up.
+// The table broke up, below two seats: the hands each seat still at it
+// won.
export interface RummyGameEnded {
- winner?: string
- points: number
- scores: RummyScore[]
+ standings: RummyStanding[]
+ dealsPlayed: number
}
// The rummy update union's JSON encoding: exactly one member present.
@@ -90,6 +124,7 @@ export interface RummyMovePayloads {
joinGame: { gameId: string }
startGame: undefined
leaveGame: undefined
+ chooseVariant: { variant: string }
drawStock: undefined
drawDiscard: undefined
meld: { cards: Card[] }
diff --git a/src/hooks/__tests__/useRummyTable.test.tsx b/src/hooks/__tests__/useRummyTable.test.tsx
index f2cb37c1..2816aa0d 100644
--- a/src/hooks/__tests__/useRummyTable.test.tsx
+++ b/src/hooks/__tests__/useRummyTable.test.tsx
@@ -20,6 +20,12 @@ const seat = (playerId: string, hand: string[] = []): RummyPlayer => ({
const view = (over: Partial = {}): RummyView => ({
gameId: 'GAME01',
phase: 'playing',
+ variant: 'basic',
+ dealNumber: 1,
+ standings: [
+ { playerId: 'bob', handsWon: 0 },
+ { playerId: 'alice', handsWon: 0 }
+ ],
players: [seat('bob'), seat('alice', ['7♥', '8♥', '9♥', 'K♣'])],
currentPlayerId: 'alice',
stage: 'play',
@@ -135,21 +141,37 @@ describe('useRummyTable', () => {
expect(mount().result.current.order).toBe('rank')
})
- it('a turn is not a toast; the deal and another table opening are', () => {
+ it('a turn and the table starting are the felt’s to show; another table opening is a toast', () => {
const { receive, showNotice } = mount()
receive({ turnChanged: { playerId: 'alice' } })
- expect(showNotice).not.toHaveBeenCalled()
receive({ gameStarted: {} })
+ expect(showNotice).not.toHaveBeenCalled()
receive({ gameCreated: { gameId: 'G2', createdBy: 'bob' } })
receive({ gameCreated: { gameId: 'G3', createdBy: 'alice' } })
- expect(showNotice.mock.calls).toEqual([['Dealt. Draw a card to open your turn.'], ['bob opened table G2']])
+ expect(showNotice.mock.calls).toEqual([['bob opened table G2']])
+ })
+
+ it('the dealer’s pick names the variant, once, until the hub answers', () => {
+ const { result, move, receive } = mount()
+ act(() => result.current.chooseVariant('basic'))
+ expect(move.mock.calls).toEqual([['chooseVariant', { variant: 'basic' }]])
+ expect(result.current.dealing).toBe(true)
+ receive({ gameState: { view: view() } })
+ expect(result.current.dealing).toBe(false)
+ act(() => result.current.chooseVariant('basic'))
+ act(() => result.current.handleRejected())
+ expect(result.current.dealing).toBe(false)
+ // A table joined meanwhile answers it too.
+ act(() => result.current.chooseVariant('basic'))
+ receive({ gameJoined: { view: view() } })
+ expect(result.current.dealing).toBe(false)
})
it('play again opens another table; the ending goes with the old one', () => {
const { result, receive, move } = mount()
receive({ gameState: { view: view({ phase: 'ended' }) } })
- receive({ gameEnded: { winner: 'alice', points: 30, scores: [] } })
- expect(result.current.ended?.winner).toBe('alice')
+ receive({ gameEnded: { standings: [{ playerId: 'alice', handsWon: 2 }], dealsPlayed: 3 } })
+ expect(result.current.ended?.dealsPlayed).toBe(3)
act(() => result.current.playAgain())
expect(move.mock.calls).toEqual([['createGame']])
expect(result.current.opening).toBe(true)
diff --git a/src/hooks/useRummyTable.ts b/src/hooks/useRummyTable.ts
index b4603e2e..e8f26c7b 100644
--- a/src/hooks/useRummyTable.ts
+++ b/src/hooks/useRummyTable.ts
@@ -15,7 +15,9 @@ const storedOrder = (): HandOrder => (safeLocalStorage.get(ORDER_KEY) === 'rank'
// What the table's chrome calls; the lobby panel adds create and join.
export interface RummyTableActions {
+ // Seats the table; the dealer's pick deals.
startTable: () => void
+ chooseVariant: (variant: string) => void
leaveTable: () => void
// Another table, from the one that just ended: a create, since the
// finished one is already gone from the hub.
@@ -44,6 +46,9 @@ export interface UseRummyTable extends RummyTableActions {
order: HandOrder
// A table has been asked for and not yet arrived.
opening: boolean
+ // A deal has been asked for and the hub has not answered: a second ask
+ // would be refused and read as the first having failed.
+ dealing: boolean
handleUpdate: (update: RummyUpdate) => void
// Any refusal: whatever it was for, the table asked for did not
// happen, so the ask can be made again.
@@ -67,6 +72,7 @@ export const useRummyTable = ({ playerId, move, showNotice, onLeft }: UseRummyTa
const [ended, setEnded] = useState(null)
const [selected, setSelected] = useState([])
const [opening, setOpening] = useState(false)
+ const [dealing, setDealing] = useState(false)
const [order, setOrderState] = useState(storedOrder)
const clear = useCallback(() => {
@@ -74,8 +80,12 @@ export const useRummyTable = ({ playerId, move, showNotice, onLeft }: UseRummyTa
setEnded(null)
setSelected([])
setOpening(false)
+ setDealing(false)
+ }, [])
+ const handleRejected = useCallback(() => {
+ setOpening(false)
+ setDealing(false)
}, [])
- const handleRejected = useCallback(() => setOpening(false), [])
const handleUpdate = useCallback(
(update: RummyUpdate) => {
@@ -84,24 +94,23 @@ export const useRummyTable = ({ playerId, move, showNotice, onLeft }: UseRummyTa
setEnded(null)
setSelected([])
setOpening(false)
+ setDealing(false)
return
}
if (update.gameState) {
setView(update.gameState.view)
setSelected([])
+ setDealing(false)
return
}
if (update.gameCreated) {
if (update.gameCreated.createdBy !== playerId) showNotice(`${update.gameCreated.createdBy} opened table ${update.gameCreated.gameId}`)
return
}
- if (update.gameStarted) {
- showNotice('Dealt. Draw a card to open your turn.')
- return
- }
- if (update.turnChanged) {
- // The felt lights the seat on turn and the piles light up for its
- // draw; a toast at the foot of the screen would sit on the hand.
+ if (update.gameStarted || update.turnChanged) {
+ // The felt says both: who deals between deals, and the seat on turn
+ // with its piles lit. A toast at the foot of the screen would sit
+ // on the hand.
return
}
if (update.gameEnded) {
@@ -125,6 +134,13 @@ export const useRummyTable = ({ playerId, move, showNotice, onLeft }: UseRummyTa
}, [move])
const joinTable = useCallback((gameId: string) => move('joinGame', { gameId }), [move])
const startTable = useCallback(() => move('startGame'), [move])
+ const chooseVariant = useCallback(
+ (variant: string) => {
+ setDealing(true)
+ move('chooseVariant', { variant })
+ },
+ [move]
+ )
const leaveTable = useCallback(() => {
if (view !== null && view.phase !== 'ended') {
move('leaveGame')
@@ -188,12 +204,14 @@ export const useRummyTable = ({ playerId, move, showNotice, onLeft }: UseRummyTa
selected,
order,
opening,
+ dealing,
handleUpdate,
handleRejected,
clear,
createTable,
joinTable,
startTable,
+ chooseVariant,
leaveTable,
playAgain,
drawStock,