test: add NPC possession reference differential

This commit is contained in:
2026-07-31 04:38:17 +00:00
parent b720c20be4
commit cc05cb11a7
3 changed files with 570 additions and 7 deletions
@@ -1,6 +1,6 @@
import { randomInt } from 'node:crypto';
import { asNumber, asRecord, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import { asNumber, asRecord, JosaUtil, LiteHashDRBG, RandUtil, type RNG } from '@sammo-ts/common';
import { GamePrisma, type DatabaseClient, type GamePrisma as GamePrismaTypes } from '@sammo-ts/infra';
import {
ActionLogger,
@@ -71,6 +71,11 @@ export interface NpcPossessionReservation {
candidates: NpcPossessionCandidate[];
}
export interface NpcPossessionSelectionObserver {
onRandomDraw?: (value: number) => void;
onCandidateDraw?: (selectedId: string) => void;
}
interface NpcSelectionTokenRow {
ownerUserId: string;
validUntil: Date;
@@ -238,10 +243,11 @@ const buildCandidateSnapshot = async (
};
};
const chooseCandidates = (
export const chooseNpcPossessionCandidates = (
candidates: NpcPossessionCandidate[],
kept: Record<string, NpcPossessionCandidate>,
rng: RandUtil
rng: RandUtil,
onDraw?: (selectedId: string) => void
): Record<string, NpcPossessionCandidate> => {
const picked = { ...kept };
const weights = Object.fromEntries(
@@ -254,6 +260,7 @@ const chooseCandidates = (
const pickLimit = Math.min(candidates.length, MAX_PICK_COUNT);
while (Object.keys(picked).length < pickLimit) {
const selectedId = String(rng.choiceUsingWeight(weights));
onDraw?.(selectedId);
if (!Object.hasOwn(picked, selectedId)) {
const candidate = byId.get(selectedId);
if (!candidate) {
@@ -265,6 +272,21 @@ const chooseCandidates = (
return picked;
};
class ObservedRandUtil extends RandUtil {
constructor(
rng: RNG,
private readonly onRandomDraw: (value: number) => void
) {
super(rng);
}
public override nextFloat1(): number {
const value = super.nextFloat1();
this.onRandomDraw(value);
return value;
}
}
export const reserveNpcPossessionCandidates = async (options: {
db: DatabaseClient;
worldState: WorldStateRow;
@@ -273,6 +295,7 @@ export const reserveNpcPossessionCandidates = async (options: {
refresh?: boolean;
keepIds?: number[];
now?: Date;
selectionObserver?: NpcPossessionSelectionObserver;
}): Promise<NpcPossessionReservation> => {
const { db, worldState, userId } = options;
requireNpcPossessionWorld(worldState);
@@ -375,10 +398,13 @@ export const reserveNpcPossessionCandidates = async (options: {
const candidates = await Promise.all(
generalRows.map((row) => buildCandidateSnapshot(row, nations.get(row.nationId)))
);
const rng = new RandUtil(
new LiteHashDRBG(buildNpcSelectionTokenSeed(readHiddenSeed(worldState), options.ownerIdentity, now))
const selectionRng = new LiteHashDRBG(
buildNpcSelectionTokenSeed(readHiddenSeed(worldState), options.ownerIdentity, now)
);
const pickResult = chooseCandidates(candidates, kept, rng);
const rng = options.selectionObserver?.onRandomDraw
? new ObservedRandUtil(selectionRng, options.selectionObserver.onRandomDraw)
: new RandUtil(selectionRng);
const pickResult = chooseNpcPossessionCandidates(candidates, kept, rng, options.selectionObserver?.onCandidateDraw);
const turnTermMinutes = resolveTurnTermMinutes(worldState);
const validUntil = new Date(now.getTime() + Math.max(VALID_SECONDS, turnTermMinutes * 40) * 1000);
const refreshedPickMoreFrom = new Date(