feat: add bond registry with helpers for mutual relationships
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
makePairKey, createBondRegistry, addBond, hasBond,
|
||||
getBonds, dissolveBond, getActiveBondCount,
|
||||
} from '../bondRegistry.js';
|
||||
|
||||
describe('bondRegistry', () => {
|
||||
describe('makePairKey', () => {
|
||||
it('should order IDs canonically (lower first)', () => {
|
||||
expect(makePairKey(5, 3)).toBe('3:5');
|
||||
expect(makePairKey(3, 5)).toBe('3:5');
|
||||
});
|
||||
|
||||
it('should handle equal IDs', () => {
|
||||
expect(makePairKey(7, 7)).toBe('7:7');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addBond', () => {
|
||||
it('should add a bond between two entities', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
expect(hasBond(registry, 1, 2, 'partner')).toBe(true);
|
||||
expect(hasBond(registry, 2, 1, 'partner')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not duplicate bond types for same pair', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
addBond(registry, 1, 2, 'partner', 200);
|
||||
const bonds = getBonds(registry, 1, 2);
|
||||
expect(bonds.filter(b => b.type === 'partner')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should allow multiple bond types for same pair', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
addBond(registry, 1, 2, 'sibling', 200);
|
||||
const bonds = getBonds(registry, 1, 2);
|
||||
expect(bonds).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasBond', () => {
|
||||
it('should return false when no bonds exist', () => {
|
||||
const registry = createBondRegistry();
|
||||
expect(hasBond(registry, 1, 2, 'partner')).toBe(false);
|
||||
});
|
||||
|
||||
it('should check active status by default', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
dissolveBond(registry, 1, 2, 'partner');
|
||||
expect(hasBond(registry, 1, 2, 'partner')).toBe(false);
|
||||
});
|
||||
|
||||
it('should find former bonds when specified', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
dissolveBond(registry, 1, 2, 'partner');
|
||||
expect(hasBond(registry, 1, 2, 'partner', 'former')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dissolveBond', () => {
|
||||
it('should mark bond as former', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
dissolveBond(registry, 1, 2, 'partner');
|
||||
const bonds = getBonds(registry, 1, 2);
|
||||
expect(bonds[0].status).toBe('former');
|
||||
});
|
||||
|
||||
it('should be a no-op if bond does not exist', () => {
|
||||
const registry = createBondRegistry();
|
||||
dissolveBond(registry, 1, 2, 'partner');
|
||||
expect(getBonds(registry, 1, 2)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveBondCount', () => {
|
||||
it('should count active bonds of a type for an entity', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
addBond(registry, 1, 3, 'partner', 200);
|
||||
expect(getActiveBondCount(registry, 1, 'partner')).toBe(2);
|
||||
expect(getActiveBondCount(registry, 2, 'partner')).toBe(1);
|
||||
});
|
||||
|
||||
it('should not count former bonds', () => {
|
||||
const registry = createBondRegistry();
|
||||
addBond(registry, 1, 2, 'partner', 100);
|
||||
dissolveBond(registry, 1, 2, 'partner');
|
||||
expect(getActiveBondCount(registry, 1, 'partner')).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { EntityId } from '@dflike/shared';
|
||||
|
||||
export interface Bond {
|
||||
type: string;
|
||||
formedAtTick: number;
|
||||
status: 'active' | 'former';
|
||||
}
|
||||
|
||||
export type BondRegistry = Map<string, Bond[]>;
|
||||
|
||||
export function createBondRegistry(): BondRegistry {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export function makePairKey(a: EntityId, b: EntityId): string {
|
||||
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
||||
}
|
||||
|
||||
export function addBond(
|
||||
registry: BondRegistry, a: EntityId, b: EntityId,
|
||||
type: string, tick: number,
|
||||
): void {
|
||||
const key = makePairKey(a, b);
|
||||
if (!registry.has(key)) registry.set(key, []);
|
||||
const bonds = registry.get(key)!;
|
||||
if (bonds.some(b => b.type === type && b.status === 'active')) return;
|
||||
bonds.push({ type, formedAtTick: tick, status: 'active' });
|
||||
}
|
||||
|
||||
export function getBonds(registry: BondRegistry, a: EntityId, b: EntityId): Bond[] {
|
||||
return registry.get(makePairKey(a, b)) ?? [];
|
||||
}
|
||||
|
||||
export function hasBond(
|
||||
registry: BondRegistry, a: EntityId, b: EntityId,
|
||||
type: string, status: 'active' | 'former' = 'active',
|
||||
): boolean {
|
||||
return getBonds(registry, a, b).some(b => b.type === type && b.status === status);
|
||||
}
|
||||
|
||||
export function dissolveBond(
|
||||
registry: BondRegistry, a: EntityId, b: EntityId, type: string,
|
||||
): void {
|
||||
const bonds = getBonds(registry, a, b);
|
||||
const bond = bonds.find(b => b.type === type && b.status === 'active');
|
||||
if (bond) bond.status = 'former';
|
||||
}
|
||||
|
||||
export function getActiveBondCount(
|
||||
registry: BondRegistry, entity: EntityId, type: string,
|
||||
): number {
|
||||
let count = 0;
|
||||
for (const [key, bonds] of registry) {
|
||||
const [a, b] = key.split(':').map(Number);
|
||||
if (a !== entity && b !== entity) continue;
|
||||
count += bonds.filter(bond => bond.type === type && bond.status === 'active').length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
Reference in New Issue
Block a user