ts refactor
This commit is contained in:
179
src/rules-scoring.ts
Normal file
179
src/rules-scoring.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { EnergySimulation, GameState, NodeKey, PlayerId, RootBurst, RoundAnimation } from "./types";
|
||||
import { keyFor, parseKey, shuffleArray } from "./utils";
|
||||
import { buildChildrenMap, buildParentMap } from "./rules-board";
|
||||
|
||||
export function buildEnergySimulation(state: GameState): EnergySimulation {
|
||||
const parentMap = buildParentMap(state);
|
||||
const columns = [];
|
||||
const scores = state.players.map(() => 0);
|
||||
|
||||
for (let column = 0; column < state.config.columns; column += 1) {
|
||||
let hitNodeKey: NodeKey | null = null;
|
||||
|
||||
for (let row = 0; row < state.config.rows; row += 1) {
|
||||
const nodeKey = keyFor(row, column);
|
||||
if (state.nodes.has(nodeKey)) {
|
||||
hitNodeKey = nodeKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hitNodeKey) {
|
||||
columns.push({
|
||||
column,
|
||||
terminalRow: state.config.rows - 1,
|
||||
intercepted: false,
|
||||
ownerId: null,
|
||||
hitNode: null,
|
||||
rootKey: null,
|
||||
branchNodes: [],
|
||||
branchEdges: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const hitNode = parseKey(hitNodeKey);
|
||||
const ownerId = state.nodes.get(hitNodeKey)?.ownerId as PlayerId;
|
||||
const branchNodes = [hitNode];
|
||||
const branchEdges = [];
|
||||
let cursor = hitNodeKey;
|
||||
|
||||
while (parentMap.has(cursor)) {
|
||||
const parentKey = parentMap.get(cursor) as NodeKey;
|
||||
branchEdges.push({ from: parseKey(cursor), to: parseKey(parentKey) });
|
||||
branchNodes.push(parseKey(parentKey));
|
||||
cursor = parentKey;
|
||||
}
|
||||
|
||||
scores[ownerId] += 1;
|
||||
columns.push({
|
||||
column,
|
||||
terminalRow: hitNode.row,
|
||||
intercepted: true,
|
||||
ownerId,
|
||||
hitNode,
|
||||
rootKey: cursor,
|
||||
branchNodes,
|
||||
branchEdges,
|
||||
});
|
||||
}
|
||||
|
||||
const rootBurstMap = columns.reduce((map, column) => {
|
||||
if (!column.intercepted || !column.rootKey) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const entry = map.get(column.rootKey) ?? { key: column.rootKey, playerId: column.ownerId as PlayerId, count: 0 };
|
||||
entry.count += 1;
|
||||
map.set(column.rootKey, entry);
|
||||
return map;
|
||||
}, new Map<NodeKey, RootBurst>());
|
||||
const rootBursts: RootBurst[] = [...rootBurstMap.values()];
|
||||
|
||||
return {
|
||||
scores,
|
||||
columns,
|
||||
rootBursts,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRoundAnimation(
|
||||
state: GameState,
|
||||
energySimulation: EnergySimulation,
|
||||
sunbeamPlayerId: PlayerId | null,
|
||||
diseaseKeys: NodeKey[],
|
||||
): RoundAnimation {
|
||||
const traces = energySimulation.columns
|
||||
.filter((column) => column.intercepted)
|
||||
.map((column) => ({
|
||||
playerId: column.ownerId as PlayerId,
|
||||
verticalCells: Array.from({ length: column.terminalRow + 1 }, (_, row) => ({ row, column: column.column })),
|
||||
ray: {
|
||||
x: ((column.column + 0.5) / state.config.columns) * 100,
|
||||
y: ((column.terminalRow + 0.5) / state.config.rows) * 100,
|
||||
},
|
||||
rootKey: column.rootKey,
|
||||
branchNodes: column.branchNodes,
|
||||
}));
|
||||
|
||||
const bonusTrace = sunbeamPlayerId === null ? null : traces.find((trace) => trace.playerId === sunbeamPlayerId) ?? null;
|
||||
const bonusBurst = bonusTrace ? energySimulation.rootBursts.find((burst) => burst.key === bonusTrace.rootKey) ?? null : null;
|
||||
|
||||
return {
|
||||
phase: "sunlight",
|
||||
columns: energySimulation.columns,
|
||||
traces,
|
||||
rootBursts: energySimulation.rootBursts,
|
||||
sunbeamPlayerId,
|
||||
bonusTrace,
|
||||
bonusBurst,
|
||||
diseaseKeys,
|
||||
};
|
||||
}
|
||||
|
||||
export function scoreColumns(state: GameState) {
|
||||
const energySimulation = buildEnergySimulation(state);
|
||||
const columnResults = energySimulation.columns.map((column) => ({
|
||||
column: column.column,
|
||||
ownerId: column.ownerId,
|
||||
topRow: column.intercepted ? column.terminalRow : null,
|
||||
tied: false,
|
||||
}));
|
||||
|
||||
return { scores: energySimulation.scores, columnResults, energySimulation };
|
||||
}
|
||||
|
||||
export function maybeRollSunbeam(state: GameState, scores: number[]) {
|
||||
const nextGrowth = scores.map((score) => score + 1);
|
||||
const { sunbeamChance } = state.randomEffects;
|
||||
|
||||
if (sunbeamChance <= 0 || Math.random() * 100 >= sunbeamChance) {
|
||||
return {
|
||||
nextGrowth,
|
||||
event: null,
|
||||
awardedPlayer: null,
|
||||
};
|
||||
}
|
||||
|
||||
const awardedPlayer = Math.floor(Math.random() * state.players.length);
|
||||
nextGrowth[awardedPlayer] += 1;
|
||||
|
||||
return {
|
||||
nextGrowth,
|
||||
awardedPlayer,
|
||||
event: `${state.players[awardedPlayer].name} caught a stray sunbeam and gains +1 growth next round.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function maybeRollDisease(state: GameState) {
|
||||
const { diseaseChance } = state.randomEffects;
|
||||
if (diseaseChance <= 0 || Math.random() * 100 >= diseaseChance) {
|
||||
return {
|
||||
killedKeys: [],
|
||||
event: null,
|
||||
};
|
||||
}
|
||||
|
||||
const childrenMap = buildChildrenMap(state);
|
||||
const parentMap = buildParentMap(state);
|
||||
const twigKeys = Array.from(state.nodes.keys()).filter((nodeKey) => {
|
||||
const { row } = parseKey(nodeKey);
|
||||
return row !== state.config.rows - 1 && !childrenMap.has(nodeKey) && parentMap.has(nodeKey);
|
||||
});
|
||||
|
||||
if (twigKeys.length === 0) {
|
||||
return {
|
||||
killedKeys: [],
|
||||
event: null,
|
||||
};
|
||||
}
|
||||
|
||||
const shuffled = shuffleArray(twigKeys);
|
||||
const killCount = Math.min(shuffled.length, 1 + Math.floor(Math.random() * Math.min(3, shuffled.length)));
|
||||
const killedKeys = shuffled.slice(0, killCount);
|
||||
|
||||
return {
|
||||
killedKeys,
|
||||
event: `Disease spread through ${killCount} twig node${killCount === 1 ? "" : "s"} before the next round.`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user