Skip to content
10 changes: 7 additions & 3 deletions src/apps/castle/components/CastleTable.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -675,13 +675,14 @@ down and to the right so the back's corner shows under it. */

/* Small or short screens: smaller cards, and the other seats fold their
fan away for the count — except at the showdown, which is every hand
face up and nothing to fold. */
face up and nothing to fold: a game's end, or rummy between deals
(data-showdown). */
@media (max-width: 600px), (max-height: 900px) {
.table:not([data-phase='ended']) .seat:not(.mine) .hand {
.table:not([data-phase='ended']):not([data-showdown]) .seat:not(.mine) .hand {
display: none;
}

.table:not([data-phase='ended']) .seat:not(.mine) .handCount {
.table:not([data-phase='ended']):not([data-showdown]) .seat:not(.mine) .handCount {
display: inline;
}

Expand Down Expand Up @@ -939,6 +940,9 @@ down and to the right so the back's corner shows under it. */
padding: 1.5rem;
border-radius: 1rem;
background: rgba(12, 8, 32, 0.96);
/* Portaled to the body, so it inherits the page's text colour, not the
felt's: set its own for the dark card. */
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
text-align: center;
Expand Down
4 changes: 3 additions & 1 deletion src/apps/lobby/components/LobbyGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const LobbyGame = (props: UseLobbyProps) => {
const lobby = useLobby(props)
const { castle, golf, rummy, chat, connected, playerId, notice, room } = lobby
const atTable = castle.view !== null || golf.view !== null || rummy.view !== null
// A rummy dealer among these lets any seat deal (MoonBase#1609).
const away = (room?.players ?? []).filter(player => !player.connected).map(player => player.playerId)
// A table takes the screen, so the panel folds away when one comes up
// and returns to what the player wants when it goes.
const [panelOpen, setPanelOpen] = useState(panelWanted)
Expand Down Expand Up @@ -103,7 +105,7 @@ const LobbyGame = (props: UseLobbyProps) => {
)}
{rummy.view !== null && (
<div className={styles.tableOverlay}>
<RummyTable playerId={playerId} connected={connected} view={rummy.view} table={rummy} />
<RummyTable playerId={playerId} connected={connected} view={rummy.view} table={rummy} away={away} />
</div>
)}
{golf.view !== null && (
Expand Down
2 changes: 1 addition & 1 deletion src/apps/lobby/components/LobbyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const LobbyPanel = ({ lobby, roomCodeRef }: LobbyPanelProps) => {
return (
<li key={table.gameId} className={styles.row}>
<span>
{game} {table.gameId} · {table.playerCount}/{TABLE_SEATS} · {table.status}
{game} {table.gameId} · {table.playerCount}/{TABLE_SEATS} · {table.status === 'choosing' ? 'between deals' : table.status}
</span>
<button
type="button"
Expand Down
24 changes: 23 additions & 1 deletion src/apps/lobby/components/__tests__/LobbyGame.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ vi.mock('@/apps/thoughts/components/ThoughtsGame', () => ({
}
}))
vi.mock('@/apps/castle/components/CastleTable', () => ({ default: () => <div>table</div> }))
vi.mock('@/apps/rummy/components/RummyTable', () => ({ default: () => <div>rummy table</div> }))
vi.mock('@/apps/rummy/components/RummyTable', () => ({
default: ({ away }: { away?: string[] }) => <div>rummy table{away !== undefined && away.length > 0 ? `, away: ${away.join(' ')}` : ''}</div>
}))
const openChat = vi.fn()
const askBot = vi.fn()
vi.mock('../RoomChat', () => ({
Expand Down Expand Up @@ -152,6 +154,26 @@ describe('LobbyGame', () => {
expect(screen.getByRole('complementary', { name: 'lobby' })).toBeTruthy()
})

// Who the room shows gone is what lets a seat deal for an away dealer.
it('hands the rummy table the seats the room shows away', () => {
state.room = {
roomId: 'R1',
games: [],
players: [
{ playerId: 'alice', connected: true, gamesPlayed: 0, gamesWon: 0, totalScore: 0 },
{ playerId: 'bob', connected: false, gamesPlayed: 0, gamesWon: 0, totalScore: 0 }
]
} as unknown as UseLobby['room']
state.rummy.view = { gameId: 'M1' } as RummyView
try {
render(<LobbyGame />)
expect(screen.getByText('rummy table, away: bob')).toBeTruthy()
} finally {
state.rummy.view = null
state.room = null
}
})

// The bare world is the panel hidden on purpose, and on purpose means
// it stays hidden.
describe('a hidden panel', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/apps/lobby/components/__tests__/LobbyPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ describe('LobbyPanel', () => {
expect(hook.joinRoom).toHaveBeenCalledTimes(1)
})

it('a rummy table between deals reads as that, not as its wire status', () => {
render(<LobbyPanel lobby={lobby({ room: room({ games: [{ gameId: 'M2', game: 'rummy', status: 'choosing', playerCount: 3 }] }) })} />)
expect(screen.getByText('rummy M2 · 3/4 · between deals')).toBeTruthy()
})

it('in a room, reads presence off each member and offers only open tables', () => {
const hook = lobby({ room: room() })
render(<LobbyPanel lobby={hook} />)
Expand Down
68 changes: 59 additions & 9 deletions src/apps/rummy/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { describe, expect, it } from 'vitest'
import {
arrangedMeld,
canDeal,
canDiscard,
deadwood,
describeEnding,
describeLastMove,
describeTableEnd,
headlineOf,
meldsFitting,
sortHand
sortHand,
variantLabel
} from '../rules'
import type { Card, RummyView } from '../wire'

Expand Down Expand Up @@ -90,6 +93,8 @@ describe('canDiscard', () => {
const view = (over: Partial<RummyView> = {}): RummyView => ({
gameId: 'G',
phase: 'playing',
dealNumber: 1,
standings: [],
players: [],
stockCount: 0,
canDrawStock: true,
Expand Down Expand Up @@ -118,18 +123,63 @@ describe('describeLastMove', () => {
})
})

describe('the ending', () => {
const won = { winner: 'alice', points: 42, scores: [] }
describe('a deal’s end', () => {
const won = { variant: 'basic', winner: 'alice', points: 42, scores: [] }
it('reads from each chair', () => {
expect(headlineOf(won, 'alice')).toBe('You won!')
expect(headlineOf(won, 'bob')).toBe('alice wins')
expect(headlineOf(won, 'alice')).toBe('You won the hand!')
expect(headlineOf(won, 'bob')).toBe('alice wins the hand')
expect(describeEnding(won, 'alice')).toBe('You went out and score 42 points.')
expect(describeEnding(won, 'bob')).toBe('alice went out and scores 42 points.')
expect(describeEnding({ winner: 'alice', points: 1, scores: [] }, 'bob')).toBe('alice went out and scores 1 point.')
expect(describeEnding({ ...won, points: 1 }, 'bob')).toBe('alice went out and scores 1 point.')
})
it('names nobody for a table that broke up', () => {
const broke = { points: 0, scores: [] }
expect(headlineOf(broke, 'alice')).toBe('The table broke up')
it('names nobody for a deal that broke up', () => {
const broke = { variant: 'basic', points: 0, scores: [] }
expect(headlineOf(broke, 'alice')).toBe('The deal broke up')
expect(describeEnding(broke, 'alice')).toBe('Nobody went out.')
})
})

describe('the table’s end', () => {
const standings = [
{ playerId: 'alice', handsWon: 2 },
{ playerId: 'bob', handsWon: 1 }
]
it('reads the hands each chair won', () => {
expect(describeTableEnd({ standings, dealsPlayed: 3 }, 'alice')).toBe('You won 2 of 3 hands.')
expect(describeTableEnd({ standings, dealsPlayed: 1 }, 'bob')).toBe('You won 1 of 1 hand.')
})
it('says what was played to a chair no longer in the standings', () => {
expect(describeTableEnd({ standings, dealsPlayed: 3 }, 'carol')).toBe('3 hands played.')
expect(describeTableEnd({ standings: [], dealsPlayed: 0 }, 'alice')).toBe('No hands played.')
})
})

describe('the dealer’s choice', () => {
const choosing = (dealer: string): RummyView => ({
gameId: 'M1',
phase: 'choosing',
players: [],
stockCount: 0,
canDrawStock: false,
discardCount: 0,
melds: [],
dealNumber: 1,
standings: [],
choosing: { dealer, options: ['basic'] }
})
it('is the dealer’s alone while they are here', () => {
expect(canDeal(choosing('alice'), 'alice', [])).toBe(true)
expect(canDeal(choosing('bob'), 'alice', [])).toBe(false)
})
it('passes to anyone while the dealer is away', () => {
expect(canDeal(choosing('bob'), 'alice', ['bob'])).toBe(true)
expect(canDeal(choosing('bob'), 'alice', ['carol'])).toBe(false)
})
it('is nobody’s outside the choosing', () => {
expect(canDeal({ ...choosing('alice'), phase: 'playing', choosing: undefined }, 'alice', [])).toBe(false)
})
it('names each variant, and an unknown one as it came', () => {
expect(variantLabel('basic')).toBe('Basic rummy')
expect(variantLabel('gin')).toBe('gin')
})
})
2 changes: 1 addition & 1 deletion src/apps/rummy/components/RummyTable.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ button.meld {
outline-offset: -4px;
}

/* A hand of ten or eleven is wider than castle's three. */
/* A hand of seven or eight is wider than castle's three. */
.hand {
--overlap-max: 1.7rem;
}
Expand Down
Loading
Loading