316 lines
8.1 KiB
TypeScript
316 lines
8.1 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"),
|
|
},
|
|
mostPorts: null,
|
|
mostPortCount: 0,
|
|
sessions: {},
|
|
unselected: [],
|
|
placements: {
|
|
corners: [],
|
|
roads: [],
|
|
},
|
|
turn: {
|
|
name: "",
|
|
color: "unassigned",
|
|
actions: [],
|
|
limits: {},
|
|
roll: 0,
|
|
volcano: null,
|
|
free: false,
|
|
freeRoads: 0,
|
|
select: {},
|
|
active: null,
|
|
robberInAction: false,
|
|
placedRobber: false,
|
|
developmentPurchased: false,
|
|
offer: null,
|
|
},
|
|
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;
|
|
};
|