1
0

305 lines
7.8 KiB
TypeScript

import randomWords from "random-words";
import { games, gameDB } from "./store";
import { PLAYER_COLORS, type Game } from "./types";
import { addChatMessage, clearPlayer, stopTurnTimer } from "./helpers";
import newPlayer from "./playerFactory";
import { info } from "./constants";
import { layout, staticData } from "../../util/layout";
import { shuffleArray } from "./utils";
import { pickRobber } from "./robber";
import { audio } from "../webrtc-signaling";
export const resetGame = (game: any) => {
Object.assign(game, {
startTime: Date.now(),
state: "lobby",
turns: 0,
step: 0 /* used for the suffix # in game backups */,
turn: {},
sheep: 19,
ore: 19,
wool: 19,
brick: 19,
wheat: 19,
placements: {
corners: [],
roads: [],
},
developmentCards: [],
chat: [],
activities: [],
pipOrder: game.pipOrder,
borderOrder: game.borderOrder,
tileOrder: game.tileOrder,
signature: game.signature,
players: game.players,
stolen: {
robber: {
stole: {
total: 0,
},
},
total: 0,
},
longestRoad: "",
longestRoadLength: 0,
largestArmy: "",
largestArmySize: 0,
mostDeveloped: "",
mostDevelopmentCards: 0,
mostPorts: "",
mostPortCount: 0,
winner: undefined,
active: 0,
});
stopTurnTimer(game);
/* Populate the game corner and road placement data as cleared */
for (let i = 0; i < layout.corners.length; i++) {
game.placements.corners[i] = {
color: undefined,
type: undefined,
};
}
for (let i = 0; i < layout.roads.length; i++) {
game.placements.roads[i] = {
color: undefined,
longestRoad: undefined,
};
}
/* Put the robber back on the Desert */
for (let i = 0; i < game.pipOrder.length; i++) {
if (game.pipOrder[i] === 18) {
game.robber = i;
break;
}
}
/* Populate the game development cards with a fresh deck */
for (let i = 1; i <= 14; i++) {
game.developmentCards.push({
type: "army",
card: i,
});
}
["monopoly", "monopoly", "road-1", "road-2", "year-of-plenty", "year-of-plenty"].forEach((card) =>
game.developmentCards.push({
type: "progress",
card: card,
})
);
["market", "library", "palace", "university"].forEach((card) =>
game.developmentCards.push({
type: "vp",
card: card,
})
);
shuffleArray(game.developmentCards);
/* Reset all player data, and add in any missing colors */
PLAYER_COLORS.forEach((color) => {
if (color in game.players) {
clearPlayer(game.players[color]);
} else {
game.players[color] = newPlayer(color);
}
});
/* Ensure sessions are connected to player objects */
for (let key in game.sessions) {
const session = game.sessions[key];
if (session.color !== "unassigned") {
game.active++;
session.player = game.players[session.color];
session.player.status = "Active";
session.player.lastActive = Date.now();
session.player.live = session.live;
session.player.name = session.name;
session.player.color = session.color;
}
}
game.animationSeeds = [];
for (let i = 0; i < game.tileOrder.length; i++) {
game.animationSeeds.push(Math.random());
}
};
export const setBeginnerGame = (game: Game) => {
pickRobber(game);
shuffleArray(game.developmentCards);
game.borderOrder = [];
for (let i = 0; i < 6; i++) {
game.borderOrder.push(i);
}
game.tileOrder = [9, 12, 1, 5, 16, 13, 17, 6, 2, 0, 3, 10, 4, 11, 7, 14, 18, 8, 15];
game.robber = 9;
game.animationSeeds = [];
for (let i = 0; i < game.tileOrder.length; i++) {
game.animationSeeds.push(Math.random());
}
game.pipOrder = [5, 1, 6, 7, 2, 9, 11, 12, 8, 18, 3, 4, 10, 16, 13, 0, 14, 15, 17];
game.signature = gameSignature(game);
};
export const shuffleBoard = (game: any): void => {
pickRobber(game);
const seq = [];
for (let i = 0; i < 6; i++) {
seq.push(i);
}
shuffleArray(seq);
game.borderOrder = seq.slice();
for (let i = 6; i < 19; i++) {
seq.push(i);
}
shuffleArray(seq);
game.tileOrder = seq.slice();
/* Pip order is from one of the random corners, then rotate around
* and skip over the desert (robber) */
/* Board:
* 0 1 2
* 3 4 5 6
* 7 8 9 10 11
* 12 13 14 15
* 16 17 18
*/
const order = [
[0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 4, 5, 10, 14, 13, 8, 9],
[2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 5, 10, 14, 13, 8, 4, 9],
[11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 10, 14, 13, 8, 4, 5, 9],
[18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 14, 13, 8, 4, 5, 10, 9],
[16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 13, 8, 4, 5, 10, 14, 9],
[7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 8, 4, 5, 10, 14, 13, 9],
];
const sequence = order[Math.floor(Math.random() * order.length)];
if (!sequence || !Array.isArray(sequence)) {
// Defensive: should not happen, but guard for TS strictness
return;
}
game.pipOrder = [];
game.animationSeeds = [];
for (let i = 0, p = 0; i < sequence.length; i++) {
const target: number = sequence[i]!;
/* If the target tile is the desert (18), then set the
* pip value to the robber (18) otherwise set
* the target pip value to the currently incremeneting
* pip value. */
const tileIdx = game.tileOrder[target];
const tileType = staticData.tiles[tileIdx]?.type;
if (!game.pipOrder) game.pipOrder = [];
if (tileType === "desert") {
game.robber = target;
game.pipOrder[target] = 18;
} else {
game.pipOrder[target] = p++;
}
game.animationSeeds.push(Math.random());
}
shuffleArray(game.developmentCards);
game.signature = gameSignature(game);
};
export const createGame = async (id: string | null = null) => {
/* Look for a new game with random words that does not already exist */
while (!id) {
id = randomWords(4).join("-");
try {
/* If a game with this id exists in the DB, look for a new name */
if (!gameDB || !gameDB.getGameById) {
throw new Error("Game DB not available for uniqueness check");
}
let exists = false;
try {
const g = await gameDB.getGameById(id);
if (g) exists = true;
} catch (e) {
// if DB check fails treat as non-existent and continue searching
}
if (exists) {
id = "";
}
} catch (error) {
break;
}
}
console.log(`${info}: creating ${id}`);
const game: Game = {
id: id,
developmentCards: [],
playerOrder: [],
turns: 0,
players: {
O: newPlayer("O"),
R: newPlayer("R"),
B: newPlayer("B"),
W: newPlayer("W"),
},
sessions: {},
unselected: [],
placements: {
corners: [],
roads: [],
},
turn: {
name: "",
color: "unassigned",
actions: [],
limits: {},
roll: 0,
},
rules: {
"victory-points": {
points: 10,
},
},
pipOrder: [],
borderOrder: [],
tileOrder: [],
step: 0 /* used for the suffix # in game backups */,
};
setBeginnerGame(game);
resetGame(game);
addChatMessage(game, null, `New game created with Beginner's Layout: ${game.id}`);
games[game.id] = game;
audio[game.id] = {};
return game;
};
const gameSignature = (game: Game): string => {
if (!game) {
return "";
}
const salt = 251;
const signature =
(game.borderOrder || []).map((border: any) => `00${(Number(border) ^ salt).toString(16)}`.slice(-2)).join("") +
"-" +
(game.pipOrder || [])
.map((pip: any, index: number) => `00${(Number(pip) ^ salt ^ (salt * index)).toString(16)}`.slice(-2))
.join("") +
"-" +
(game.tileOrder || [])
.map((tile: any, index: number) => `00${(Number(tile) ^ salt ^ (salt * index)).toString(16)}`.slice(-2))
.join("");
return signature;
};