feat(three-monks): Add tool effect statistics tracking
This commit is contained in:
parent
572b473089
commit
6fa1f511bd
|
|
@ -1,5 +1,6 @@
|
|||
import type { Entity, World } from "../../src/index";
|
||||
import { query } from "../../src/query";
|
||||
import type { ToolEffectRecorder } from "./stats";
|
||||
import {
|
||||
ActionCard,
|
||||
CARRYING_TOOLS,
|
||||
|
|
@ -82,20 +83,20 @@ export function collectProposals(world: World): RoundProposals {
|
|||
return proposals;
|
||||
}
|
||||
|
||||
export function resolveRound(world: World): void {
|
||||
export function resolveRound(world: World, stats?: ToolEffectRecorder): void {
|
||||
const state = world.getSingleton(GameState);
|
||||
if (state.phase === "gameOver") return;
|
||||
|
||||
state.phase = "resolving";
|
||||
const proposals = collectProposals(world);
|
||||
|
||||
if (proposals.chant) resolveChant(world);
|
||||
if (proposals.fetchWater) resolveFetchWater(world);
|
||||
if (proposals.exchange) resolveExchange(world);
|
||||
if (proposals.drink) resolveDrink(world);
|
||||
if (proposals.chant) resolveChant(world, stats);
|
||||
if (proposals.fetchWater) resolveFetchWater(world, stats);
|
||||
if (proposals.exchange) resolveExchange(world, stats);
|
||||
if (proposals.drink) resolveDrink(world, stats);
|
||||
if (world.getSingleton(GameState).phase === "gameOver") return;
|
||||
|
||||
resolveEndOfRoundTools(world);
|
||||
resolveEndOfRoundTools(world, stats);
|
||||
checkWinner(world);
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +119,10 @@ export function prepareNextRound(world: World): void {
|
|||
state.message = `Round ${world.getSingleton(Table).round}: choose an action card.`;
|
||||
}
|
||||
|
||||
export function resolveFetchWater(world: World): void {
|
||||
export function resolveFetchWater(
|
||||
world: World,
|
||||
stats?: ToolEffectRecorder,
|
||||
): void {
|
||||
const table = world.getSingleton(Table);
|
||||
const players = getPlayersInSeatOrder(world);
|
||||
const participants = players.filter((player) => {
|
||||
|
|
@ -141,6 +145,12 @@ export function resolveFetchWater(world: World): void {
|
|||
if (playerData.water >= placed) {
|
||||
playerData.water -= placed;
|
||||
tool.water += placed;
|
||||
stats?.record(tool.kind, "挑水", {
|
||||
playerWaterDelta: -placed,
|
||||
ownToolWaterDelta: placed,
|
||||
});
|
||||
} else {
|
||||
stats?.record(tool.kind, "挑水", {}, "水不足,未放水");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,11 +161,18 @@ export function resolveFetchWater(world: World): void {
|
|||
if (tool.kind === "ladle" && table.centralWater > 0) {
|
||||
table.centralWater--;
|
||||
tool.water++;
|
||||
stats?.record(tool.kind, "挑水", {
|
||||
ownToolWaterDelta: 1,
|
||||
centralWaterDelta: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveExchange(world: World): void {
|
||||
export function resolveExchange(
|
||||
world: World,
|
||||
stats?: ToolEffectRecorder,
|
||||
): void {
|
||||
const participants = getPlayersInSeatOrder(world).filter(
|
||||
(player) => getSelectedAction(world, player) !== "chant",
|
||||
);
|
||||
|
|
@ -167,8 +184,13 @@ export function resolveExchange(world: World): void {
|
|||
for (const toolEntity of tools) {
|
||||
const tool = world.get(toolEntity, Tool);
|
||||
if (tool.kind === "shoulderPole" && tool.water > 0) {
|
||||
table.centralWater += tool.water;
|
||||
const dumped = tool.water;
|
||||
table.centralWater += dumped;
|
||||
tool.water = 0;
|
||||
stats?.record(tool.kind, "交换", {
|
||||
ownToolWaterDelta: -dumped,
|
||||
centralWaterDelta: dumped,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +201,7 @@ export function resolveExchange(world: World): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveDrink(world: World): void {
|
||||
export function resolveDrink(world: World, stats?: ToolEffectRecorder): void {
|
||||
for (const player of getPlayersFromMarker(world)) {
|
||||
if (!canParticipateInDrink(world, player)) continue;
|
||||
|
||||
|
|
@ -199,10 +221,17 @@ export function resolveDrink(world: World): void {
|
|||
tool.water -= maxFromTool;
|
||||
playerData.water += maxFromTool;
|
||||
drankFromTool = maxFromTool;
|
||||
stats?.record(tool.kind, "喝水", {
|
||||
playerWaterDelta: maxFromTool,
|
||||
ownToolWaterDelta: -maxFromTool,
|
||||
});
|
||||
}
|
||||
|
||||
if (tool.kind === "waterJar") {
|
||||
playerData.water++;
|
||||
stats?.record(tool.kind, "喝水", {
|
||||
playerWaterDelta: 1,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -212,6 +241,10 @@ export function resolveDrink(world: World): void {
|
|||
) {
|
||||
table.centralWater--;
|
||||
tool.water++;
|
||||
stats?.record(tool.kind, "喝水", {
|
||||
ownToolWaterDelta: 1,
|
||||
centralWaterDelta: -1,
|
||||
});
|
||||
}
|
||||
|
||||
if (playerData.water >= 10) {
|
||||
|
|
@ -221,7 +254,7 @@ export function resolveDrink(world: World): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function resolveChant(world: World): void {
|
||||
export function resolveChant(world: World, stats?: ToolEffectRecorder): void {
|
||||
const chanters = getPlayersFromMarker(world).filter(
|
||||
(player) => getSelectedAction(world, player) === "chant",
|
||||
);
|
||||
|
|
@ -235,17 +268,32 @@ export function resolveChant(world: World): void {
|
|||
|
||||
if (tool.kind === "bottle") {
|
||||
tool.water++;
|
||||
stats?.record(tool.kind, "念经", {
|
||||
ownToolWaterDelta: 1,
|
||||
});
|
||||
} else if (tool.kind === "mouse") {
|
||||
moveOneNeighborWaterTo(world, player, toolEntity);
|
||||
const moved = moveOneNeighborWaterTo(world, player, toolEntity);
|
||||
if (moved) {
|
||||
stats?.record(tool.kind, "念经", {
|
||||
ownToolWaterDelta: 1,
|
||||
otherToolWaterDelta: -1,
|
||||
});
|
||||
} else {
|
||||
stats?.record(tool.kind, "念经", {}, "相邻道具无水可偷");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveEndOfRoundTools(world: World): void {
|
||||
export function resolveEndOfRoundTools(
|
||||
world: World,
|
||||
stats?: ToolEffectRecorder,
|
||||
): void {
|
||||
for (const toolEntity of world.query(query(Tool))) {
|
||||
const tool = world.get(toolEntity, Tool);
|
||||
if (tool.kind === "woodenFish") {
|
||||
world.getSingleton(WoodenFishMarker).holder = tool.owner;
|
||||
stats?.record(tool.kind, "轮末", {}, "获得木鱼标记");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +319,7 @@ function moveOneNeighborWaterTo(
|
|||
world: World,
|
||||
player: Entity,
|
||||
destinationTool: Entity,
|
||||
): void {
|
||||
): boolean {
|
||||
const neighbors = [
|
||||
getLeftPlayer(world, player),
|
||||
getRightPlayer(world, player),
|
||||
|
|
@ -282,9 +330,10 @@ function moveOneNeighborWaterTo(
|
|||
if (neighborTool.water > 0) {
|
||||
neighborTool.water--;
|
||||
world.get(destinationTool, Tool).water++;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setWinner(world: World, player: Entity): void {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getPlayersInSeatOrder,
|
||||
type ActionKind,
|
||||
} from "./components";
|
||||
import { ToolEffectStats, formatToolEffectSummaries } from "./stats";
|
||||
import {
|
||||
beginSelectionPhase,
|
||||
checkWinner,
|
||||
|
|
@ -52,7 +53,7 @@ interface GameStats {
|
|||
waterDiff: number;
|
||||
}
|
||||
|
||||
function runSingleGame(seed: number): GameStats {
|
||||
function runSingleGame(seed: number, stats?: ToolEffectStats): GameStats {
|
||||
const random = mulberry32(seed);
|
||||
const world = new World();
|
||||
const playerNames = ["慧空", "明心", "了尘", "净远"];
|
||||
|
|
@ -76,13 +77,13 @@ function runSingleGame(seed: number): GameStats {
|
|||
|
||||
const proposals = collectProposals(world);
|
||||
|
||||
if (proposals.chant) resolveChant(world);
|
||||
if (proposals.fetchWater) resolveFetchWater(world);
|
||||
if (proposals.exchange) resolveExchange(world);
|
||||
if (proposals.drink) resolveDrink(world);
|
||||
if (proposals.chant) resolveChant(world, stats);
|
||||
if (proposals.fetchWater) resolveFetchWater(world, stats);
|
||||
if (proposals.exchange) resolveExchange(world, stats);
|
||||
if (proposals.drink) resolveDrink(world, stats);
|
||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||
|
||||
resolveEndOfRoundTools(world);
|
||||
resolveEndOfRoundTools(world, stats);
|
||||
checkWinner(world);
|
||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||
|
||||
|
|
@ -104,12 +105,13 @@ function runSingleGame(seed: number): GameStats {
|
|||
export function runSimulation(gameCount = 100, startSeed = 20260701): void {
|
||||
let totalRounds = 0;
|
||||
let totalWaterDiff = 0;
|
||||
const toolStats = new ToolEffectStats();
|
||||
|
||||
console.log(`开始模拟 ${gameCount} 局游戏...`);
|
||||
|
||||
for (let i = 0; i < gameCount; i++) {
|
||||
const seed = startSeed + i;
|
||||
const stats = runSingleGame(seed);
|
||||
const stats = runSingleGame(seed, toolStats);
|
||||
totalRounds += stats.rounds;
|
||||
totalWaterDiff += stats.waterDiff;
|
||||
}
|
||||
|
|
@ -122,6 +124,10 @@ export function runSimulation(gameCount = 100, startSeed = 20260701): void {
|
|||
console.log(`结束时平均轮数: ${avgRounds.toFixed(2)} 轮`);
|
||||
console.log(`结束时首尾玩家平均水差距: ${avgWaterDiff.toFixed(2)} 口水`);
|
||||
console.log("==========================================");
|
||||
console.log("\n道具效果统计(总变化量):");
|
||||
for (const line of formatToolEffectSummaries(toolStats)) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1]?.endsWith("simulate.ts")) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import type { ToolKind } from "./components";
|
||||
import { TOOL_LABELS } from "./logging";
|
||||
|
||||
export interface ToolEffectDelta {
|
||||
playerWaterDelta?: number;
|
||||
ownToolWaterDelta?: number;
|
||||
otherToolWaterDelta?: number;
|
||||
centralWaterDelta?: number;
|
||||
}
|
||||
|
||||
export interface ToolEffectRecord extends Required<ToolEffectDelta> {
|
||||
tool: ToolKind;
|
||||
phase: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ToolEffectSummary extends Required<ToolEffectDelta> {
|
||||
tool: ToolKind;
|
||||
activations: number;
|
||||
}
|
||||
|
||||
export interface ToolEffectRecorder {
|
||||
record(tool: ToolKind, phase: string, delta?: ToolEffectDelta, description?: string): void;
|
||||
}
|
||||
|
||||
export class ToolEffectStats implements ToolEffectRecorder {
|
||||
private readonly summaries = new Map<ToolKind, ToolEffectSummary>();
|
||||
private readonly records: ToolEffectRecord[] = [];
|
||||
|
||||
record(
|
||||
tool: ToolKind,
|
||||
phase: string,
|
||||
delta: ToolEffectDelta = {},
|
||||
description?: string,
|
||||
): void {
|
||||
const record: ToolEffectRecord = {
|
||||
tool,
|
||||
phase,
|
||||
description,
|
||||
playerWaterDelta: delta.playerWaterDelta ?? 0,
|
||||
ownToolWaterDelta: delta.ownToolWaterDelta ?? 0,
|
||||
otherToolWaterDelta: delta.otherToolWaterDelta ?? 0,
|
||||
centralWaterDelta: delta.centralWaterDelta ?? 0,
|
||||
};
|
||||
|
||||
this.records.push(record);
|
||||
|
||||
const summary = this.summaries.get(tool) ?? {
|
||||
tool,
|
||||
activations: 0,
|
||||
playerWaterDelta: 0,
|
||||
ownToolWaterDelta: 0,
|
||||
otherToolWaterDelta: 0,
|
||||
centralWaterDelta: 0,
|
||||
};
|
||||
|
||||
summary.activations++;
|
||||
summary.playerWaterDelta += record.playerWaterDelta;
|
||||
summary.ownToolWaterDelta += record.ownToolWaterDelta;
|
||||
summary.otherToolWaterDelta += record.otherToolWaterDelta;
|
||||
summary.centralWaterDelta += record.centralWaterDelta;
|
||||
this.summaries.set(tool, summary);
|
||||
}
|
||||
|
||||
getRecords(): readonly ToolEffectRecord[] {
|
||||
return this.records;
|
||||
}
|
||||
|
||||
getSummaries(): ToolEffectSummary[] {
|
||||
return [...this.summaries.values()].sort((a, b) =>
|
||||
TOOL_LABELS[a.tool].localeCompare(TOOL_LABELS[b.tool], "zh-Hans-CN"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatToolEffectSummaries(stats: ToolEffectStats): string[] {
|
||||
const summaries = stats.getSummaries();
|
||||
if (summaries.length === 0) return ["无道具效果记录"];
|
||||
|
||||
return [
|
||||
"道具 | 触发次数 | 玩家水变化 | 自身道具水变化 | 其他道具水变化 | 中央水变化",
|
||||
"--- | ---: | ---: | ---: | ---: | ---:",
|
||||
...summaries.map((summary) =>
|
||||
[
|
||||
TOOL_LABELS[summary.tool],
|
||||
summary.activations,
|
||||
summary.playerWaterDelta,
|
||||
summary.ownToolWaterDelta,
|
||||
summary.otherToolWaterDelta,
|
||||
summary.centralWaterDelta,
|
||||
].join(" | "),
|
||||
),
|
||||
];
|
||||
}
|
||||
Loading…
Reference in New Issue