*/ private function candidates(): array { $rows = [ ['id' => 1, 'leadership' => 40, 'strength' => 50, 'intel' => 60, 'keepCnt' => 3], ['id' => 2, 'leadership' => 73, 'strength' => 44, 'intel' => 81, 'keepCnt' => 3], ['id' => 3, 'leadership' => 91, 'strength' => 32, 'intel' => 47, 'keepCnt' => 3], ['id' => 4, 'leadership' => 55, 'strength' => 88, 'intel' => 38, 'keepCnt' => 3], ['id' => 5, 'leadership' => 66, 'strength' => 67, 'intel' => 68, 'keepCnt' => 3], ['id' => 6, 'leadership' => 99, 'strength' => 20, 'intel' => 21, 'keepCnt' => 3], ['id' => 7, 'leadership' => 35, 'strength' => 93, 'intel' => 72, 'keepCnt' => 3], ['id' => 8, 'leadership' => 84, 'strength' => 75, 'intel' => 69, 'keepCnt' => 3], ]; $result = []; foreach ($rows as $row) { $result[$row['id']] = $row; } return $result; } public function testSharedKernelPreservesThePreviousEndpointLoop(): void { $candidates = $this->candidates(); $weights = []; foreach ($candidates as $id => $candidate) { $weights[$id] = pow( $candidate['leadership'] + $candidate['strength'] + $candidate['intel'], 1.5, ); self::assertSame($weights[$id], NpcPossessionSelector::weight($candidate)); } $seed = NpcPossessionSelector::buildSeed( 'npc-possession-differential-seed', 7701, '2026-08-01 00:00:01', ); $legacyRng = new RandUtil(new LiteHashDRBG($seed)); $legacyPicked = []; $legacyDraws = []; $pickLimit = min(count($candidates), 5); while (count($legacyPicked) < $pickLimit) { $id = $legacyRng->choiceUsingWeight($weights); $legacyDraws[] = (int)$id; if (!array_key_exists($id, $legacyPicked)) { $legacyPicked[$id] = $candidates[$id]; } } $sharedDraws = []; $sharedPicked = NpcPossessionSelector::select( $candidates, $weights, [], new RandUtil(new LiteHashDRBG($seed)), static function (int|string $id) use (&$sharedDraws): void { $sharedDraws[] = (int)$id; }, ); self::assertSame([1, 8, 1, 4, 5, 5, 8, 8, 5, 3], $legacyDraws); self::assertSame($legacyDraws, $sharedDraws); self::assertSame($legacyPicked, $sharedPicked); } public function testKeepAndReservedHelpersPreserveEndpointSemantics(): void { $candidates = $this->candidates(); $weights = array_map( static fn(array $candidate): float => NpcPossessionSelector::weight($candidate), $candidates, ); NpcPossessionSelector::removeReserved( $candidates, $weights, [ Json::encode((object)[2 => ['keepCnt' => 3], 7 => ['keepCnt' => 3]]), ], ); self::assertSame([1, 3, 4, 5, 6, 8], array_keys($candidates)); self::assertSame([1, 3, 4, 5, 6, 8], array_keys($weights)); $oldPick = array_intersect_key($this->candidates(), array_flip([1, 8, 4, 5, 3])); $partial = NpcPossessionSelector::applyKeep($oldPick, [1, 4]); self::assertFalse($partial['cancelled']); self::assertSame(2, $partial['picked'][1]['keepCnt']); self::assertSame(2, $partial['picked'][4]['keepCnt']); $all = NpcPossessionSelector::applyKeep($oldPick, [1, 8, 4, 5, 3]); self::assertTrue($all['cancelled']); self::assertSame([1, 8, 4, 5, 3], array_keys($all['picked'])); } }