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 type { Entity, World } from "../../src/index";
|
||||||
import { query } from "../../src/query";
|
import { query } from "../../src/query";
|
||||||
|
import type { ToolEffectRecorder } from "./stats";
|
||||||
import {
|
import {
|
||||||
ActionCard,
|
ActionCard,
|
||||||
CARRYING_TOOLS,
|
CARRYING_TOOLS,
|
||||||
|
|
@ -82,20 +83,20 @@ export function collectProposals(world: World): RoundProposals {
|
||||||
return proposals;
|
return proposals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRound(world: World): void {
|
export function resolveRound(world: World, stats?: ToolEffectRecorder): void {
|
||||||
const state = world.getSingleton(GameState);
|
const state = world.getSingleton(GameState);
|
||||||
if (state.phase === "gameOver") return;
|
if (state.phase === "gameOver") return;
|
||||||
|
|
||||||
state.phase = "resolving";
|
state.phase = "resolving";
|
||||||
const proposals = collectProposals(world);
|
const proposals = collectProposals(world);
|
||||||
|
|
||||||
if (proposals.chant) resolveChant(world);
|
if (proposals.chant) resolveChant(world, stats);
|
||||||
if (proposals.fetchWater) resolveFetchWater(world);
|
if (proposals.fetchWater) resolveFetchWater(world, stats);
|
||||||
if (proposals.exchange) resolveExchange(world);
|
if (proposals.exchange) resolveExchange(world, stats);
|
||||||
if (proposals.drink) resolveDrink(world);
|
if (proposals.drink) resolveDrink(world, stats);
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") return;
|
if (world.getSingleton(GameState).phase === "gameOver") return;
|
||||||
|
|
||||||
resolveEndOfRoundTools(world);
|
resolveEndOfRoundTools(world, stats);
|
||||||
checkWinner(world);
|
checkWinner(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +119,10 @@ export function prepareNextRound(world: World): void {
|
||||||
state.message = `Round ${world.getSingleton(Table).round}: choose an action card.`;
|
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 table = world.getSingleton(Table);
|
||||||
const players = getPlayersInSeatOrder(world);
|
const players = getPlayersInSeatOrder(world);
|
||||||
const participants = players.filter((player) => {
|
const participants = players.filter((player) => {
|
||||||
|
|
@ -141,6 +145,12 @@ export function resolveFetchWater(world: World): void {
|
||||||
if (playerData.water >= placed) {
|
if (playerData.water >= placed) {
|
||||||
playerData.water -= placed;
|
playerData.water -= placed;
|
||||||
tool.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) {
|
if (tool.kind === "ladle" && table.centralWater > 0) {
|
||||||
table.centralWater--;
|
table.centralWater--;
|
||||||
tool.water++;
|
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(
|
const participants = getPlayersInSeatOrder(world).filter(
|
||||||
(player) => getSelectedAction(world, player) !== "chant",
|
(player) => getSelectedAction(world, player) !== "chant",
|
||||||
);
|
);
|
||||||
|
|
@ -167,8 +184,13 @@ export function resolveExchange(world: World): void {
|
||||||
for (const toolEntity of tools) {
|
for (const toolEntity of tools) {
|
||||||
const tool = world.get(toolEntity, Tool);
|
const tool = world.get(toolEntity, Tool);
|
||||||
if (tool.kind === "shoulderPole" && tool.water > 0) {
|
if (tool.kind === "shoulderPole" && tool.water > 0) {
|
||||||
table.centralWater += tool.water;
|
const dumped = tool.water;
|
||||||
|
table.centralWater += dumped;
|
||||||
tool.water = 0;
|
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)) {
|
for (const player of getPlayersFromMarker(world)) {
|
||||||
if (!canParticipateInDrink(world, player)) continue;
|
if (!canParticipateInDrink(world, player)) continue;
|
||||||
|
|
||||||
|
|
@ -199,10 +221,17 @@ export function resolveDrink(world: World): void {
|
||||||
tool.water -= maxFromTool;
|
tool.water -= maxFromTool;
|
||||||
playerData.water += maxFromTool;
|
playerData.water += maxFromTool;
|
||||||
drankFromTool = maxFromTool;
|
drankFromTool = maxFromTool;
|
||||||
|
stats?.record(tool.kind, "喝水", {
|
||||||
|
playerWaterDelta: maxFromTool,
|
||||||
|
ownToolWaterDelta: -maxFromTool,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tool.kind === "waterJar") {
|
if (tool.kind === "waterJar") {
|
||||||
playerData.water++;
|
playerData.water++;
|
||||||
|
stats?.record(tool.kind, "喝水", {
|
||||||
|
playerWaterDelta: 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -212,6 +241,10 @@ export function resolveDrink(world: World): void {
|
||||||
) {
|
) {
|
||||||
table.centralWater--;
|
table.centralWater--;
|
||||||
tool.water++;
|
tool.water++;
|
||||||
|
stats?.record(tool.kind, "喝水", {
|
||||||
|
ownToolWaterDelta: 1,
|
||||||
|
centralWaterDelta: -1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerData.water >= 10) {
|
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(
|
const chanters = getPlayersFromMarker(world).filter(
|
||||||
(player) => getSelectedAction(world, player) === "chant",
|
(player) => getSelectedAction(world, player) === "chant",
|
||||||
);
|
);
|
||||||
|
|
@ -235,17 +268,32 @@ export function resolveChant(world: World): void {
|
||||||
|
|
||||||
if (tool.kind === "bottle") {
|
if (tool.kind === "bottle") {
|
||||||
tool.water++;
|
tool.water++;
|
||||||
|
stats?.record(tool.kind, "念经", {
|
||||||
|
ownToolWaterDelta: 1,
|
||||||
|
});
|
||||||
} else if (tool.kind === "mouse") {
|
} 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))) {
|
for (const toolEntity of world.query(query(Tool))) {
|
||||||
const tool = world.get(toolEntity, Tool);
|
const tool = world.get(toolEntity, Tool);
|
||||||
if (tool.kind === "woodenFish") {
|
if (tool.kind === "woodenFish") {
|
||||||
world.getSingleton(WoodenFishMarker).holder = tool.owner;
|
world.getSingleton(WoodenFishMarker).holder = tool.owner;
|
||||||
|
stats?.record(tool.kind, "轮末", {}, "获得木鱼标记");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +319,7 @@ function moveOneNeighborWaterTo(
|
||||||
world: World,
|
world: World,
|
||||||
player: Entity,
|
player: Entity,
|
||||||
destinationTool: Entity,
|
destinationTool: Entity,
|
||||||
): void {
|
): boolean {
|
||||||
const neighbors = [
|
const neighbors = [
|
||||||
getLeftPlayer(world, player),
|
getLeftPlayer(world, player),
|
||||||
getRightPlayer(world, player),
|
getRightPlayer(world, player),
|
||||||
|
|
@ -282,9 +330,10 @@ function moveOneNeighborWaterTo(
|
||||||
if (neighborTool.water > 0) {
|
if (neighborTool.water > 0) {
|
||||||
neighborTool.water--;
|
neighborTool.water--;
|
||||||
world.get(destinationTool, Tool).water++;
|
world.get(destinationTool, Tool).water++;
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWinner(world: World, player: Entity): void {
|
function setWinner(world: World, player: Entity): void {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getPlayersInSeatOrder,
|
getPlayersInSeatOrder,
|
||||||
type ActionKind,
|
type ActionKind,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
import { ToolEffectStats, formatToolEffectSummaries } from "./stats";
|
||||||
import {
|
import {
|
||||||
beginSelectionPhase,
|
beginSelectionPhase,
|
||||||
checkWinner,
|
checkWinner,
|
||||||
|
|
@ -52,7 +53,7 @@ interface GameStats {
|
||||||
waterDiff: number;
|
waterDiff: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runSingleGame(seed: number): GameStats {
|
function runSingleGame(seed: number, stats?: ToolEffectStats): GameStats {
|
||||||
const random = mulberry32(seed);
|
const random = mulberry32(seed);
|
||||||
const world = new World();
|
const world = new World();
|
||||||
const playerNames = ["慧空", "明心", "了尘", "净远"];
|
const playerNames = ["慧空", "明心", "了尘", "净远"];
|
||||||
|
|
@ -76,13 +77,13 @@ function runSingleGame(seed: number): GameStats {
|
||||||
|
|
||||||
const proposals = collectProposals(world);
|
const proposals = collectProposals(world);
|
||||||
|
|
||||||
if (proposals.chant) resolveChant(world);
|
if (proposals.chant) resolveChant(world, stats);
|
||||||
if (proposals.fetchWater) resolveFetchWater(world);
|
if (proposals.fetchWater) resolveFetchWater(world, stats);
|
||||||
if (proposals.exchange) resolveExchange(world);
|
if (proposals.exchange) resolveExchange(world, stats);
|
||||||
if (proposals.drink) resolveDrink(world);
|
if (proposals.drink) resolveDrink(world, stats);
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||||
|
|
||||||
resolveEndOfRoundTools(world);
|
resolveEndOfRoundTools(world, stats);
|
||||||
checkWinner(world);
|
checkWinner(world);
|
||||||
if (world.getSingleton(GameState).phase === "gameOver") break;
|
if (world.getSingleton(GameState).phase === "gameOver") break;
|
||||||
|
|
||||||
|
|
@ -104,12 +105,13 @@ function runSingleGame(seed: number): GameStats {
|
||||||
export function runSimulation(gameCount = 100, startSeed = 20260701): void {
|
export function runSimulation(gameCount = 100, startSeed = 20260701): void {
|
||||||
let totalRounds = 0;
|
let totalRounds = 0;
|
||||||
let totalWaterDiff = 0;
|
let totalWaterDiff = 0;
|
||||||
|
const toolStats = new ToolEffectStats();
|
||||||
|
|
||||||
console.log(`开始模拟 ${gameCount} 局游戏...`);
|
console.log(`开始模拟 ${gameCount} 局游戏...`);
|
||||||
|
|
||||||
for (let i = 0; i < gameCount; i++) {
|
for (let i = 0; i < gameCount; i++) {
|
||||||
const seed = startSeed + i;
|
const seed = startSeed + i;
|
||||||
const stats = runSingleGame(seed);
|
const stats = runSingleGame(seed, toolStats);
|
||||||
totalRounds += stats.rounds;
|
totalRounds += stats.rounds;
|
||||||
totalWaterDiff += stats.waterDiff;
|
totalWaterDiff += stats.waterDiff;
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +124,10 @@ export function runSimulation(gameCount = 100, startSeed = 20260701): void {
|
||||||
console.log(`结束时平均轮数: ${avgRounds.toFixed(2)} 轮`);
|
console.log(`结束时平均轮数: ${avgRounds.toFixed(2)} 轮`);
|
||||||
console.log(`结束时首尾玩家平均水差距: ${avgWaterDiff.toFixed(2)} 口水`);
|
console.log(`结束时首尾玩家平均水差距: ${avgWaterDiff.toFixed(2)} 口水`);
|
||||||
console.log("==========================================");
|
console.log("==========================================");
|
||||||
|
console.log("\n道具效果统计(总变化量):");
|
||||||
|
for (const line of formatToolEffectSummaries(toolStats)) {
|
||||||
|
console.log(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv[1]?.endsWith("simulate.ts")) {
|
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