merge: complete tournament betting parity
This commit is contained in:
@@ -198,6 +198,7 @@ const zAdjustGeneralResources = z.object({
|
||||
generalId: zFiniteNumber,
|
||||
goldDelta: zFiniteNumber.optional(),
|
||||
riceDelta: zFiniteNumber.optional(),
|
||||
minGoldAfter: zFiniteNumber.optional(),
|
||||
})
|
||||
.refine((value) => value.goldDelta !== undefined || value.riceDelta !== undefined)
|
||||
)
|
||||
|
||||
@@ -148,7 +148,8 @@ async function handleAdjustGeneralResources(
|
||||
}
|
||||
const nextGold = general.gold + (adjustment.goldDelta ?? 0);
|
||||
const nextRice = general.rice + (adjustment.riceDelta ?? 0);
|
||||
if (nextGold < 0 || nextRice < 0) {
|
||||
const minGoldAfter = adjustment.minGoldAfter ?? 0;
|
||||
if (nextGold < minGoldAfter || nextRice < 0) {
|
||||
return { type: 'adjustGeneralResources', ok: false, reason: '자원이 부족합니다.' };
|
||||
}
|
||||
}
|
||||
@@ -261,7 +262,7 @@ async function handleTournamentMatchResult(
|
||||
};
|
||||
}
|
||||
|
||||
const rankKey = (suffix: string): string => `rank_${prefix}${suffix}`;
|
||||
const rankKey = (suffix: string): string => `${prefix}${suffix}`;
|
||||
const getRankNumber = (general: TurnGeneral | null, key: string): number =>
|
||||
general && typeof general.meta[key] === 'number' ? Number(general.meta[key]) : 0;
|
||||
|
||||
@@ -1075,8 +1076,8 @@ async function handleTournamentBettingPayout(
|
||||
continue;
|
||||
}
|
||||
const nextMeta = { ...general.meta } as TurnGeneral['meta'];
|
||||
const betwinKey = 'rank_betwin';
|
||||
const betwingoldKey = 'rank_betwingold';
|
||||
const betwinKey = 'betwin';
|
||||
const betwingoldKey = 'betwingold';
|
||||
const currentBetwin = typeof nextMeta[betwinKey] === 'number' ? Number(nextMeta[betwinKey]) : 0;
|
||||
const currentBetwingold = typeof nextMeta[betwingoldKey] === 'number' ? Number(nextMeta[betwingoldKey]) : 0;
|
||||
nextMeta[betwinKey] = currentBetwin + delta.betwin;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { TurnSchedule } from '@sammo-ts/logic';
|
||||
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
import { createTurnDaemonCommandHandler } from '../src/turn/worldCommandHandler.js';
|
||||
|
||||
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
|
||||
|
||||
const buildGeneral = (id: number, meta: Record<string, number> = {}): TurnGeneral => ({
|
||||
id,
|
||||
name: `장수${id}`,
|
||||
nationId: 1,
|
||||
cityId: 1,
|
||||
troopId: 0,
|
||||
stats: { leadership: 50, strength: 50, intelligence: 50 },
|
||||
turnTime: new Date('0180-01-01T00:00:00Z'),
|
||||
recentWarTime: null,
|
||||
role: {
|
||||
items: { horse: null, weapon: null, book: null, item: null },
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
},
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: { killturn: 24, ...meta },
|
||||
penalty: {},
|
||||
officerLevel: 1,
|
||||
experience: 0,
|
||||
dedication: 0,
|
||||
injury: 0,
|
||||
gold: 1_000,
|
||||
rice: 1_000,
|
||||
crew: 100,
|
||||
crewTypeId: 0,
|
||||
train: 0,
|
||||
atmos: 0,
|
||||
age: 30,
|
||||
npcState: 0,
|
||||
});
|
||||
|
||||
const buildWorld = (generals: TurnGeneral[]): InMemoryTurnWorld => {
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
currentYear: 180,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 600,
|
||||
lastTurnTime: new Date('0180-01-01T00:00:00Z'),
|
||||
meta: {},
|
||||
};
|
||||
const snapshot: TurnWorldSnapshot = {
|
||||
generals,
|
||||
cities: [],
|
||||
nations: [],
|
||||
troops: [],
|
||||
diplomacy: [],
|
||||
events: [],
|
||||
initialEvents: [],
|
||||
scenarioConfig: {
|
||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||
iconPath: '',
|
||||
map: {},
|
||||
const: {},
|
||||
environment: { mapName: 'test', unitSet: 'test' },
|
||||
},
|
||||
map: {
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
cities: [],
|
||||
defaults: { trust: 50, trade: 100, supplyState: 1, frontState: 0 },
|
||||
},
|
||||
};
|
||||
return new InMemoryTurnWorld(state, snapshot, { schedule });
|
||||
};
|
||||
|
||||
describe('tournament world commands', () => {
|
||||
it('updates the persisted tt rank keys for a tournament match', async () => {
|
||||
const world = buildWorld([
|
||||
buildGeneral(1, { ttg: 10, ttw: 2 }),
|
||||
buildGeneral(2, { ttg: 5, ttl: 1 }),
|
||||
]);
|
||||
const handler = createTurnDaemonCommandHandler({ world });
|
||||
|
||||
await expect(
|
||||
handler.handle({
|
||||
type: 'tournamentMatchResult',
|
||||
tournamentType: 0,
|
||||
attackerId: 1,
|
||||
defenderId: 2,
|
||||
result: 'attacker',
|
||||
})
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
|
||||
expect(world.getGeneralById(1)?.meta).toMatchObject({ ttg: 11, ttw: 3 });
|
||||
expect(world.getGeneralById(2)?.meta).toMatchObject({ ttg: 5, ttl: 2 });
|
||||
expect(world.getGeneralById(1)?.meta).not.toHaveProperty('rank_ttg');
|
||||
});
|
||||
|
||||
it('updates persisted betting ranks when paying a winner', async () => {
|
||||
const world = buildWorld([buildGeneral(1, { betwin: 2, betwingold: 100 })]);
|
||||
const handler = createTurnDaemonCommandHandler({ world });
|
||||
|
||||
await expect(
|
||||
handler.handle({
|
||||
type: 'tournamentBettingPayout',
|
||||
bettingId: 1,
|
||||
payouts: [{ generalId: 1, amount: 500 }],
|
||||
})
|
||||
).resolves.toMatchObject({ ok: true, totalPayout: 500 });
|
||||
|
||||
expect(world.getGeneralById(1)).toMatchObject({
|
||||
gold: 1_500,
|
||||
meta: { betwin: 3, betwingold: 600 },
|
||||
});
|
||||
expect(world.getGeneralById(1)?.meta).not.toHaveProperty('rank_betwin');
|
||||
});
|
||||
|
||||
it('enforces a command-specific minimum remaining gold atomically', async () => {
|
||||
const world = buildWorld([buildGeneral(1)]);
|
||||
const handler = createTurnDaemonCommandHandler({ world });
|
||||
|
||||
await expect(
|
||||
handler.handle({
|
||||
type: 'adjustGeneralResources',
|
||||
adjustments: [{ generalId: 1, goldDelta: -501, minGoldAfter: 500 }],
|
||||
})
|
||||
).resolves.toMatchObject({ ok: false });
|
||||
expect(world.getGeneralById(1)?.gold).toBe(1_000);
|
||||
|
||||
await expect(
|
||||
handler.handle({
|
||||
type: 'adjustGeneralResources',
|
||||
adjustments: [{ generalId: 1, goldDelta: -500, minGoldAfter: 500 }],
|
||||
})
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
expect(world.getGeneralById(1)?.gold).toBe(500);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user