1
0

Compare commits

..

No commits in common. "419b36c22f283b7269c26c027b3c29c442180c7e" and "f1a5946045581c0b9f788ee89d3bcf835210394f" have entirely different histories.

11 changed files with 494 additions and 791 deletions

View File

@ -246,13 +246,7 @@ const Activities: React.FC = () => {
{placement && ( {placement && (
<div className="Requirement"> <div className="Requirement">
{who} must place { {who} must place a {placeRoad ? "road" : "settlement"}.
(turn && (turn as any).active === "road-building" && (turn as any).freeRoads)
? `${(turn as any).freeRoads} roads`
: placeRoad
? "a road"
: "a settlement"
}.
</div> </div>
)} )}

View File

@ -28,6 +28,13 @@
border: 2px dashed #666; /* Visual indicator for drop zone */ border: 2px dashed #666; /* Visual indicator for drop zone */
} }
.MediaControlSpacer.Medium {
width: 11.5em;
height: 8.625em;
/* min-width: 11.5em;
min-height: 8.625em; */
}
.MediaControl { .MediaControl {
display: flex; display: flex;
position: absolute; /* Out of flow */ position: absolute; /* Out of flow */
@ -53,6 +60,13 @@
border: 1px solid black; border: 1px solid black;
} }
.MediaControl.Medium {
width: 11.5em;
height: 8.625em;
/* min-width: 11.5em;
min-height: 8.625em; */
}
.MediaControl .Controls { .MediaControl .Controls {
display: none; /* Hidden by default, shown on hover */ display: none; /* Hidden by default, shown on hover */
position: absolute; position: absolute;

File diff suppressed because it is too large Load Diff

View File

@ -7,20 +7,18 @@ type PlayerColorProps = { color?: string };
const mapColor = (c?: string) => { const mapColor = (c?: string) => {
if (!c) return undefined; if (!c) return undefined;
switch (c.toLowerCase()) { const key = c.toLowerCase();
case 'red': switch (key) {
return 'R' case "red":
case 'orange': return "R";
return 'O' case "orange":
case 'white': return "O";
return 'W' case "white":
case 'blue': return "W";
return 'B' case "blue":
return "B";
default: default:
if (['R', 'O', 'W', 'B'].includes(c)) { return undefined;
return c
}
return undefined
} }
}; };

View File

@ -47,12 +47,31 @@
justify-content: space-around; justify-content: space-around;
} }
.PlayerList .PlayerSelector { .PlayerList .Unselected > div:nth-child(2) > div {
justify-content: flex-end;
display: flex; display: flex;
flex-direction: column;
align-items: center;
margin: 0.25rem;
padding: 0.25rem;
max-width: 8rem;
background-color: #eee;
border-radius: 0.25rem;
}
.PlayerList .Unselected .Self {
border: 1px solid black;
}
.PlayerList .PlayerSelector .PlayerColor {
width: 1em;
height: 1em;
}
.PlayerList .PlayerSelector {
display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.25rem;
flex-direction: row; flex-direction: row;
align-items: start;
} }
.PlayerList .PlayerSelector.MuiList-padding { .PlayerList .PlayerSelector.MuiList-padding {
@ -72,28 +91,30 @@
.PlayerList .PlayerSelector .PlayerEntry { .PlayerList .PlayerSelector .PlayerEntry {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; text-overflow: ellipsis;
/* min-width: 10rem; */ white-space: nowrap;
padding: 0.25rem; overflow: hidden;
flex: 1 1 0px;
align-items: flex-start;
border: 1px solid rgba(0,0,0,0);
border-radius: 0.25em;
min-width: 11em;
padding: 0 1px;
justify-content: flex-end; justify-content: flex-end;
max-width: min-content;
} }
/* .PlayerList .PlayerSelector .PlayerEntry > div:first-child { .PlayerList .PlayerSelector .PlayerEntry > div:first-child {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
margin-bottom: 0.25em; margin-bottom: 0.25em;
} */ }
/* .PlayerList .PlayerEntry[data-selectable=true]:hover { .PlayerList .PlayerEntry[data-selectable=true]:hover {
border-color: rgba(0,0,0,0.5); border-color: rgba(0,0,0,0.5);
cursor: pointer; cursor: pointer;
} */ }
/* Ensure the inline border color (player color) is visible */
/* inline style borderColor on .PlayerEntry will override background/border defaults */
.PlayerList .Players .PlayerToggle { .PlayerList .Players .PlayerToggle {
min-width: 5em; min-width: 5em;

View File

@ -1,12 +1,11 @@
import React, { useState, useEffect, useCallback, useContext, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useContext } from "react";
import Paper from '@mui/material/Paper'; import Paper from "@mui/material/Paper";
import List from '@mui/material/List'; import List from "@mui/material/List";
import './PlayerList.css'; import "./PlayerList.css";
import { MediaControl, MediaAgent, Peer } from './MediaControl'; import { MediaControl, MediaAgent, Peer } from "./MediaControl";
import { PlayerColor } from './PlayerColor'; import { PlayerColor } from "./PlayerColor";
import Box from '@mui/material/Box'; import Box from "@mui/material/Box";
import { GlobalContext } from './GlobalContext'; import { GlobalContext } from "./GlobalContext";
import { styles } from './Styles';
type Player = { type Player = {
name: string; name: string;
@ -25,17 +24,14 @@ type Player = {
const PlayerList: React.FC = () => { const PlayerList: React.FC = () => {
const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [players, setPlayers] = useState<Player[]>([]); const [players, setPlayers] = useState<Player[] | null>(null);
const [player, setPlayer] = useState<Player | null>(null);
const [peers, setPeers] = useState<Record<string, Peer>>({}); const [peers, setPeers] = useState<Record<string, Peer>>({});
const [gameState, setGameState] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
console.log('player-list - Mounted - requesting fields'); console.log("player-list - Mounted - requesting fields");
if (sendJsonMessage) { if (sendJsonMessage) {
sendJsonMessage({ sendJsonMessage({
type: 'get', type: "get",
fields: ['participants'], fields: ["participants"],
}); });
} }
}, [sendJsonMessage]); }, [sendJsonMessage]);
@ -68,6 +64,20 @@ const PlayerList: React.FC = () => {
[session] [session]
); );
useEffect(() => {
if (!players) {
return;
}
players.forEach((player) => {
console.log("rabbit - player:", {
name: player.name,
live: player.live,
in_peers: peers[player.session_id],
local_or_media: player.local || player.has_media !== false,
});
});
}, [players]);
// Use the WebSocket hook for room events with automatic reconnection // Use the WebSocket hook for room events with automatic reconnection
useEffect(() => { useEffect(() => {
if (!lastJsonMessage) { if (!lastJsonMessage) {
@ -75,33 +85,25 @@ const PlayerList: React.FC = () => {
} }
const data: any = lastJsonMessage; const data: any = lastJsonMessage;
switch (data.type) { switch (data.type) {
case 'game-update': { case "game-update": {
console.log(`player-list - game-update:`, data.update); console.log(`player-list - game-update:`, data.update);
// Track game state if provided
if ('state' in data.update) {
setGameState(data.update.state);
}
// Handle participants list // Handle participants list
if ('participants' in data.update && data.update.participants) { if ("participants" in data.update && data.update.participants) {
const participantsList: Player[] = data.update.participants; const participantsList: Player[] = data.update.participants;
console.log(`player-list - participants:`, participantsList); console.log(`player-list - participants:`, participantsList);
participantsList.forEach(player => { participantsList.forEach((player) => {
player.local = player.session_id === session?.id; player.local = player.session_id === session?.id;
if (player.local) {
setPlayer(player);
}
}); });
participantsList.sort(sortPlayers); participantsList.sort(sortPlayers);
console.log(`player-list - sorted participants:`, participantsList); console.log(`player-list - sorted participants:`, participantsList);
setPlayers(participantsList); setPlayers(participantsList);
// Initialize peers with remote mute/video state // Initialize peers with remote mute/video state
setPeers(prevPeers => { setPeers((prevPeers) => {
const updated: Record<string, Peer> = { ...prevPeers }; const updated: Record<string, Peer> = { ...prevPeers };
participantsList.forEach(player => { participantsList.forEach((player) => {
// Only update remote peers, never overwrite local peer object // Only update remote peers, never overwrite local peer object
if (!player.local && updated[player.session_id]) { if (!player.local && updated[player.session_id]) {
updated[player.session_id] = { updated[player.session_id] = {
@ -116,9 +118,9 @@ const PlayerList: React.FC = () => {
} }
break; break;
} }
case 'peer_state_update': { case "peer_state_update": {
// Update peer state in peers, but do not override local mute // Update peer state in peers, but do not override local mute
setPeers(prevPeers => { setPeers((prevPeers) => {
const updated = { ...prevPeers }; const updated = { ...prevPeers };
const peerId = data.data?.peer_id || data.peer_id; const peerId = data.data?.peer_id || data.peer_id;
if (peerId && updated[peerId]) { if (peerId && updated[peerId]) {
@ -138,68 +140,62 @@ const PlayerList: React.FC = () => {
} }
}, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]); }, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]);
// Compute which colors are already taken
const availableColors = useMemo(() => {
const assignedColors = new Set(players.filter(p => p.color !== 'unassigned').map(p => p.color));
return ['O', 'R', 'W', 'B'].filter(color => !assignedColors.has(color));
}, [players]);
useEffect(() => { useEffect(() => {
if (players.length !== 0 || !sendJsonMessage) { if (players !== null || !sendJsonMessage) {
return; return;
} }
// Request participants list // Request participants list
sendJsonMessage({ sendJsonMessage({
type: 'get', type: "get",
fields: ['participants'], fields: ["participants"],
}); });
}, [players, sendJsonMessage]); }, [players, sendJsonMessage]);
return ( return (
<Box sx={{ position: 'relative', width: '100%' }}> <Box sx={{ position: "relative", width: "100%" }}>
<Paper <Paper
className={`PlayerList Medium`} className={`player-list Medium`}
sx={{ sx={{
maxWidth: { xs: '100%', sm: 500 }, maxWidth: { xs: "100%", sm: 500 },
p: { xs: 1, sm: 2 }, p: { xs: 1, sm: 2 },
m: { xs: 0, sm: 2 }, m: { xs: 0, sm: 2 },
}} }}
> >
<MediaAgent {...{ session, peers, setPeers }} /> <MediaAgent {...{ session, peers, setPeers }} />
<List className="PlayerSelector Players"> <List className="PlayerSelector">
{players {players?.map((player) => {
.filter(p => p.color && p.color !== 'unassigned') const peerObj = peers[player.session_id] || peers[player.name];
.map(player => { return (
const peerObj = peers[player.session_id] || peers[player.name]; <Box
const playerStyle = player.color !== 'unassigned' ? styles[player.color] : {}; key={player.session_id}
return ( sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
<Box className={`PlayerEntry ${player.local ? "PlayerSelf" : ""}`}
key={player.session_id} >
sx={{ <Box>
display: 'flex', <Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
flexDirection: 'column', <Box style={{ display: "flex-wrap", alignItems: "center" }}>
alignItems: 'center', <div className="Name">{player.name ? player.name : player.session_id}</div>
p: 1, {player.protected && (
borderRadius: '0.25rem', <div
...playerStyle, style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }}
}} title="This name is protected with a password"
className={`PlayerEntry ${player.local ? 'PlayerSelf' : ''}`} >
> 🔒
<Box </div>
style={{ )}
display: 'flex-wrap', {player.bot_instance_id && (
alignItems: 'center', <div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
justifyContent: 'space-between', 🤖
}} </div>
> )}
<Box style={{ display: 'flex-wrap', alignItems: 'center' }} className="Name">
{player.bot_instance_id && <>🤖</>}
{player.name ? player.name : player.session_id}
</Box> </Box>
{player.name && !player.live && <Box className="NoNetwork"></Box>}
</Box> </Box>
{player.name && player.live ? ( {player.name && !player.live && <div className="NoNetwork"></div>}
</Box>
{player.name && player.live && peerObj && (player.local || player.has_media !== false) ? (
<>
<MediaControl <MediaControl
className="Medium"
key={player.session_id} key={player.session_id}
peer={peerObj} peer={peerObj}
isSelf={player.local} isSelf={player.local}
@ -207,99 +203,60 @@ const PlayerList: React.FC = () => {
remoteAudioMuted={peerObj?.muted} remoteAudioMuted={peerObj?.muted}
remoteVideoOff={peerObj?.video_on === false} remoteVideoOff={peerObj?.video_on === false}
/> />
) : (
<div
className="Video fade-in"
style={{
background: '#333',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
fontSize: '14px',
}}
>
💬 Chat Only
</div>
)}
</Box>
);
})}
</List>
{gameState === 'lobby' && (
<Paper sx={{ p: 0.5, mt: 0.5, backgroundColor: '#f9f9f9' }}>
<div style={{ marginBottom: 6, fontSize: '0.9em' }}>
{player && player.color !== 'unassigned' ? 'Change' : 'Pick'} your color:
</div>
<Box style={{ display: 'flex', gap: 8 }}>
{availableColors.map(c => {
return (
<Box
key={c}
sx={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
onClick={() => {
sendJsonMessage({ type: 'set', field: 'color', value: c });
}}
>
<PlayerColor color={c} />
</Box>
);
})}
</Box>
</Paper>
)}
<Paper>
<List className="PlayerSelector Observers">
{players
.filter(p => !p.color || p.color === 'unassigned')
.map(player => {
const peerObj = peers[player.session_id] || peers[player.name];
return (
<Box
key={player.session_id}
sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
className={`PlayerEntry ${player.local ? 'PlayerSelf' : ''}`}
data-selectable={player.local && gameState === 'lobby'}
>
<Box
style={{
display: 'flex-wrap',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Box style={{ display: 'flex-wrap', alignItems: 'center' }} className="Name">
{player.bot_instance_id && <>🤖</>}
{player.name ? player.name : player.session_id}
</Box>
{player.name && !player.live && <Box className="NoNetwork"></Box>}
</Box>
{/* Show media control if available */} {/* If this is the local player and they haven't picked a color, show a picker */}
{player.name && {player.local && player.color === "unassigned" && (
player.live && <div style={{ marginTop: 8, width: "100%" }}>
peerObj && <div style={{ marginBottom: 6, fontSize: "0.9em" }}>Pick your color:</div>
(player.local || player.has_media !== false) && ( <div style={{ display: "flex", gap: 8 }}>
<MediaControl {["orange", "red", "white", "blue"].map((c) => (
key={player.session_id} <Box
peer={peerObj} key={c}
isSelf={player.local} sx={{
sendJsonMessage={player.local ? sendJsonMessage : undefined} display: "flex",
remoteAudioMuted={peerObj?.muted} alignItems: "center",
remoteVideoOff={peerObj?.video_on === false} gap: 8,
/> padding: "6px 8px",
)} borderRadius: 6,
</Box> border: "1px solid #ccc",
); background: "#fff",
})} cursor: sendJsonMessage ? "pointer" : "not-allowed",
</List> }}
</Paper> onClick={() => {
if (!sendJsonMessage) return;
sendJsonMessage({ type: "set", field: "color", value: c[0].toUpperCase() });
}}
>
<PlayerColor color={c} />
</Box>
))}
</div>
</div>
)}
</>
) : player.name && player.live && player.has_media === false ? (
<div
className="Video fade-in"
style={{
background: "#333",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
fontSize: "14px",
}}
>
💬 Chat Only
</div>
) : (
<video className="Video"></video>
)}
</Box>
);
})}
</List>
</Paper> </Paper>
</Box> </Box>
); );

View File

@ -4,30 +4,18 @@ import { orange, lightBlue, red, grey } from '@mui/material/colors';
const styles = { const styles = {
R: { R: {
color: '#fff', color: '#fff',
borderWidth: 1,
borderStyle: 'solid',
borderColor: red[700],
backgroundColor: red[500], backgroundColor: red[500],
}, },
O: { O: {
color: '#000', color: '#000',
borderWidth: 1,
borderStyle: 'solid',
borderColor: orange[600],
backgroundColor: orange[500], backgroundColor: orange[500],
}, },
W: { W: {
color: '#000', color: '#000',
borderWidth: 1,
borderStyle: 'solid',
borderColor: grey[300],
backgroundColor: grey[50], backgroundColor: grey[50],
}, },
B: { B: {
color: '#fff', color: '#fff',
borderWidth: 1,
borderStyle: 'solid',
borderColor: lightBlue[700],
backgroundColor: lightBlue[500], backgroundColor: lightBlue[500],
}, },
}; };

View File

@ -4,22 +4,15 @@
align-items: center; align-items: center;
margin-right: 0.25rem; margin-right: 0.25rem;
font-size: 0.75em; font-size: 0.75em;
/* allow the Trade component to stretch to its sidebar/container */
align-self: stretch;
width: 100%;
flex-direction: column;
} }
.Trade > * { .Trade > * {
max-height: calc(100dvh - 2rem); max-height: calc(100dvh - 2rem);
overflow: auto; overflow: auto;
/* fill the available width of the sidebar rather than using a fixed size */ width: 32em;
width: 100%; display: inline-flex;
max-width: 100%;
display: flex;
padding: 0.5em; padding: 0.5em;
flex-direction: column; flex-direction: column;
box-sizing: border-box;
} }
.Trade .Title { .Trade .Title {

View File

@ -60,12 +60,7 @@ const processTies = (players: Player[]): boolean => {
/* Sort the players into buckets based on their /* Sort the players into buckets based on their
* order, and their current roll. If a resulting * order, and their current roll. If a resulting
* roll array has more than one element, then there * roll array has more than one element, then there
* is a tie that must be resolved. To ensure that * is a tie that must be resolved */
* earlier rolls remain more significant across
* subsequent tie-break rounds, we pad singleton
* players' numeric `order` when ties are detected so
* their earlier digits don't get outranked by extra
* digits appended to tied players. */
let slots: Player[][] = []; let slots: Player[][] = [];
players.forEach((player: Player) => { players.forEach((player: Player) => {
if (!slots[player.order]) { if (!slots[player.order]) {
@ -92,37 +87,6 @@ const processTies = (players: Player[]): boolean => {
} }
}; };
// First detect whether any ties exist at all. If so, we need to pad
// singleton players' `order` values (multiply by 6) so that their
// existing rolls remain more significant than any additional digits
// that will be appended by tied players when they re-roll.
for (const slot of slots) {
if (slot && slot.length > 1) {
ties = true;
break;
}
}
if (ties) {
// Pad singleton players by shifting their order a digit left in base-6
players.forEach((player: Player) => {
const slot = slots[player.order];
if (slot && slot.length === 1) {
player.order = (player.order || 0) * 6;
}
});
// Rebuild slots based on the newly padded orders so we can assign
// positions correctly below.
slots = [];
players.forEach((player: Player) => {
if (!slots[player.order]) {
slots[player.order] = [];
}
slots[player.order]!.push(player);
});
}
/* Reverse from high to low */ /* Reverse from high to low */
const rev = slots.slice().reverse(); const rev = slots.slice().reverse();
for (const slot of rev) { for (const slot of rev) {
@ -2839,43 +2803,14 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi
road.color = session.color; road.color = session.color;
road.type = "road"; road.type = "road";
player.roads--; player.roads--;
/* Handle normal play road placement including Road Building free roads.
* If the turn is a road-building action, decrement freeRoads and
* only clear actions when no free roads remain. Otherwise, during
* initial placement we advance the initial-placement sequence. */
if (game.state === "normal") {
addActivity(game, session, `${session.name} placed a road.`);
calculateRoadLengths(game, session);
let resetLimits = true; /* During initial placement, placing a road advances the initial-placement
if (game.turn && (game.turn as any).active === "road-building") { * sequence. In forward direction we move to the next player; when the
if ((game.turn as any).freeRoads !== undefined) { * last player places their road we flip to backward and begin the reverse
(game.turn as any).freeRoads = (game.turn as any).freeRoads - 1; * settlement placements. In backward direction we move to the previous
} * player and when the first player finishes, initial placement is done
if ((game.turn as any).freeRoads === 0) { * and normal play begins. */
delete (game.turn as any).free; if (game.state === "initial-placement") {
delete (game.turn as any).active;
delete (game.turn as any).freeRoads;
}
const roads = getValidRoads(game, session.color as string);
if (!roads || roads.length === 0) {
delete (game.turn as any).active;
delete (game.turn as any).freeRoads;
addActivity(game, session, `${session.name} has another road to play, but there are no more valid locations.`);
} else if ((game.turn as any).freeRoads !== 0) {
(game.turn as any).free = true;
setForRoadPlacement(game, roads);
resetLimits = false;
}
}
if (resetLimits) {
delete (game.turn as any).free;
game.turn.actions = [];
game.turn.limits = {};
}
} else if (game.state === "initial-placement") {
const order: PlayerColor[] = game.playerOrder; const order: PlayerColor[] = game.playerOrder;
const idx = order.indexOf(session.color); const idx = order.indexOf(session.color);
// defensive: if player not found, just clear actions and continue // defensive: if player not found, just clear actions and continue
@ -4894,6 +4829,3 @@ router.post("/:id?", async (req, res /*, next*/) => {
}); });
export default router; export default router;
// Export helpers for unit testing
export { processTies, processGameOrder };

View File

@ -93,12 +93,10 @@ process.on("SIGINT", () => {
server.close(() => process.exit(1)); server.close(() => process.exit(1));
}); });
if (process.env['NODE_ENV'] !== 'test') { console.log("Opening server.");
console.log("Opening server."); server.listen(serverConfig.port, () => {
server.listen(serverConfig.port, () => { console.log(`http/ws server listening on ${serverConfig.port}`);
console.log(`http/ws server listening on ${serverConfig.port}`); });
});
}
server.on("error", function (error: any) { server.on("error", function (error: any) {
if (error.syscall !== "listen") { if (error.syscall !== "listen") {

View File

@ -1,9 +1,5 @@
import request from 'supertest'; import request from 'supertest';
const { app } = require('../src/app'); const { app } = require('../src/app');
const gamesModule = require('../../server/routes/games');
// Import helpers for direct testing (exported for tests)
const { processTies } = gamesModule;
describe('Server Routes', () => { describe('Server Routes', () => {
it('should respond to GET /', async () => { it('should respond to GET /', async () => {
@ -12,43 +8,4 @@ describe('Server Routes', () => {
}); });
// Add more tests as needed // Add more tests as needed
it('resolves ties and preserves earlier rolls precedence', () => {
/* Build fake players to simulate the scenario:
* - James rolled 2
* - Guest rolled 2
* - Incognito rolled 5
* Expectation: Incognito 1st, Guest 2nd, James 3rd
* To simulate tie re-roll, processTies should mark ties and pad singleton orders.
*/
const players = [
{ name: 'James', color: 'R', order: 2, orderRoll: 2, position: '', orderStatus: '', tied: false },
{ name: 'Guest', color: 'B', order: 2, orderRoll: 2, position: '', orderStatus: '', tied: false },
{ name: 'Incognito', color: 'O', order: 5, orderRoll: 5, position: '', orderStatus: '', tied: false },
];
// First sort like the server does (descending by order)
players.sort((A: any, B: any) => B.order - A.order);
// Invoke processTies to simulate the server behavior
const hadTies = processTies(players as any);
// There should be ties among the two players who rolled 2
expect(hadTies).toBe(true);
// Incognito should be placed 1st and not tied
const inc = players.find((p: any) => p.name === 'Incognito');
expect(inc).toBeDefined();
expect(inc!.position).toBe('1st');
expect(inc!.tied).toBe(false);
// The two players who tied should have been marked tied and have orderRoll reset
const james = players.find((p: any) => p.name === 'James');
const guest = players.find((p: any) => p.name === 'Guest');
expect(james).toBeDefined();
expect(guest).toBeDefined();
expect(james!.tied).toBe(true);
expect(guest!.tied).toBe(true);
expect(james!.orderRoll).toBe(0);
expect(guest!.orderRoll).toBe(0);
});
}); });