Prototype unlimited alliance variant
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
37acaeb9d2
commit
9cb65a21f5
23 files changed
+677
-52
No files matched your search
+27
-20
@@ -1,4 +1,4 @@
|
||||
/** Foundation for the optional three-member alliance. The engine must still
|
||||
/** Foundation for optional larger alliances. The engine must still
|
||||
* authorize Nexus timing, apply legacy-field updates atomically, and connect
|
||||
* each native alliance benefit before enabling this option in play. */
|
||||
export type CoalitionSeat = { id: string; ally: string | null; allySinceTurn?: number };
|
||||
@@ -20,7 +20,7 @@ export type CoalitionAdmission = {
|
||||
nexus: string;
|
||||
turn: number;
|
||||
groupId: string;
|
||||
/** Snapshot binds the offer to the exact existing pair and its edge age. */
|
||||
/** Snapshot binds the offer to the exact existing group and its edge ages. */
|
||||
original: CoalitionGroup;
|
||||
entrant: string;
|
||||
proposer: string;
|
||||
@@ -28,6 +28,8 @@ export type CoalitionAdmission = {
|
||||
};
|
||||
export type CoalitionState = {
|
||||
version: 1;
|
||||
/** Absent preserves the existing three-member maximum. */
|
||||
variant?: 'unlimited';
|
||||
revision: number;
|
||||
groups: CoalitionGroup[];
|
||||
proposal: CoalitionAdmission | null;
|
||||
@@ -62,8 +64,8 @@ const same = (left: unknown, right: unknown) => JSON.stringify(left) === JSON.st
|
||||
const copy = <T>(value: T): T => structuredClone(value);
|
||||
const legacyId = (members: readonly string[]) => `legacy:${JSON.stringify(sorted(members))}`;
|
||||
|
||||
export function emptyCoalitionState(): CoalitionState {
|
||||
return { version: 1, revision: 0, groups: [], proposal: null, usedEvents: [] };
|
||||
export function emptyCoalitionState(variant?: 'unlimited'): CoalitionState {
|
||||
return { version: 1, ...(variant === undefined ? {} : {variant}), revision: 0, groups: [], proposal: null, usedEvents: [] };
|
||||
}
|
||||
function requireSeats(seats: readonly CoalitionSeat[]): void {
|
||||
check(Array.isArray(seats) && seats.length >= 2 && seats.length <= 6 &&
|
||||
@@ -72,9 +74,9 @@ function requireSeats(seats: readonly CoalitionSeat[]): void {
|
||||
new Set(seats.map(seat => seat.id)).size === seats.length,
|
||||
'Coalitions need two through six distinct seated players.');
|
||||
}
|
||||
function requireGroup(group: CoalitionGroup, seats: readonly CoalitionSeat[]): void {
|
||||
function requireGroup(group: CoalitionGroup, seats: readonly CoalitionSeat[], maximum: number): void {
|
||||
check(record(group) && groupId(group.id) && Array.isArray(group.members) &&
|
||||
(group.members.length === 2 || group.members.length === 3) && group.members.every(id) &&
|
||||
group.members.length >= 2 && group.members.length <= maximum && group.members.every(id) &&
|
||||
new Set(group.members).size === group.members.length && same(group.members, sorted(group.members)) &&
|
||||
group.members.every(member => seats.some(seat => seat.id === member)) && age(group.formedTurn) &&
|
||||
Array.isArray(group.edges), 'Invalid saved coalition.');
|
||||
@@ -93,7 +95,8 @@ function requireGroup(group: CoalitionGroup, seats: readonly CoalitionSeat[]): v
|
||||
check(same(actual, expected), 'A coalition must retain exactly one ordered edge for each pair.');
|
||||
}
|
||||
function requireState(state: CoalitionState): void {
|
||||
check(record(state) && state.version === 1 && Number.isSafeInteger(state.revision) && state.revision >= 0 &&
|
||||
check(record(state) && state.version === 1 && (state.variant === undefined || state.variant === 'unlimited') &&
|
||||
Number.isSafeInteger(state.revision) && state.revision >= 0 &&
|
||||
Array.isArray(state.groups) && Array.isArray(state.usedEvents) && state.usedEvents.every(id) &&
|
||||
new Set(state.usedEvents).size === state.usedEvents.length &&
|
||||
(state.proposal === null || record(state.proposal)), 'Invalid saved coalition state.');
|
||||
@@ -108,13 +111,13 @@ export function coalitionGroups(seats: readonly CoalitionSeat[], value?: Coaliti
|
||||
requireState(state);
|
||||
const seen = new Set<string>(), ids = new Set<string>();
|
||||
for (const group of state.groups) {
|
||||
requireGroup(group, seats);
|
||||
requireGroup(group, seats, state.variant === 'unlimited' ? 6 : 3);
|
||||
check(!ids.has(group.id) && group.members.every(member => !seen.has(member)),
|
||||
'A seat or coalition ID cannot belong to multiple groups.');
|
||||
ids.add(group.id); group.members.forEach(member => seen.add(member));
|
||||
if (group.members.length === 3) {
|
||||
if (group.members.length >= 3) {
|
||||
check(group.members.every(member => seats.find(seat => seat.id === member)!.ally === null),
|
||||
'Three-member groups cannot use a fictitious legacy pair.');
|
||||
'Larger groups cannot use a fictitious legacy pair.');
|
||||
} else {
|
||||
const [a, b] = group.members;
|
||||
check(seats.find(seat => seat.id === a)!.ally === b && seats.find(seat => seat.id === b)!.ally === a,
|
||||
@@ -166,10 +169,10 @@ export function coalitionGroups(seats: readonly CoalitionSeat[], value?: Coaliti
|
||||
offer.approvals.every(id) && new Set(offer.approvals).size === offer.approvals.length &&
|
||||
state.usedEvents.includes(offer.event), 'Invalid saved coalition invitation.');
|
||||
const group = groups.find(candidate => candidate.id === offer.groupId);
|
||||
check(group && group.members.length === 2 && same(group, offer.original) &&
|
||||
check(group && group.members.length < (state.variant === 'unlimited' ? 6 : 3) && same(group, offer.original) &&
|
||||
seats.some(seat => seat.id === offer.entrant) &&
|
||||
!groups.some(candidate => candidate.members.includes(offer.entrant)),
|
||||
'The saved invitation no longer matches its pair or entrant.');
|
||||
'The saved invitation no longer matches its group or entrant.');
|
||||
const participants = [...group.members, offer.entrant];
|
||||
check(participants.includes(offer.proposer) && offer.approvals.includes(offer.proposer) &&
|
||||
offer.approvals.every(actor => participants.includes(actor)) && offer.approvals.length < participants.length &&
|
||||
@@ -213,7 +216,7 @@ function unchanged(state: CoalitionState): CoalitionTransition {
|
||||
return {state, allyUpdates: {}, allySinceUpdates: {}};
|
||||
}
|
||||
|
||||
/** A proposer is either member of the pair or the free entrant; its offer is
|
||||
/** A proposer is either member of the group or the free entrant; its offer is
|
||||
* its explicit consent. A later decline by any participant cancels the offer. */
|
||||
export function beginCoalitionAdmission(seats: readonly CoalitionSeat[], value: CoalitionState | undefined,
|
||||
request: CoalitionAdmissionRequest): CoalitionTransition {
|
||||
@@ -222,19 +225,19 @@ export function beginCoalitionAdmission(seats: readonly CoalitionSeat[], value:
|
||||
const {state, groups} = currentState(seats, value, request.expectedRevision);
|
||||
check(state.proposal === null && !state.usedEvents.includes(request.event), 'Finish the current invitation and use a new event.');
|
||||
const group = groups.find(candidate => candidate.id === request.groupId);
|
||||
check(group && group.members.length === 2 && seats.some(seat => seat.id === request.entrant) &&
|
||||
check(group && group.members.length < (state.variant === 'unlimited' ? 6 : 3) && seats.some(seat => seat.id === request.entrant) &&
|
||||
!groups.some(candidate => candidate.members.includes(request.entrant)) &&
|
||||
(group.members.includes(request.proposer) || request.proposer === request.entrant) &&
|
||||
(group.formedTurn === null || group.formedTurn <= request.turn) &&
|
||||
group.edges.every(edge => (edge.formedTurn === null || edge.formedTurn <= request.turn) &&
|
||||
(!edge.legacySince || edge.legacySince.every(date => date === null || date <= request.turn))),
|
||||
'Invite one unallied seated player to an existing pair.');
|
||||
'Invite one unallied seated player to an eligible existing alliance.');
|
||||
return unchanged({ ...copy(state), revision: state.revision + 1, usedEvents: [...state.usedEvents, request.event],
|
||||
proposal: {event: request.event, nexus: request.nexus, turn: request.turn, groupId: group.id,
|
||||
original: copy(group), entrant: request.entrant, proposer: request.proposer, approvals: [request.proposer]} });
|
||||
}
|
||||
|
||||
/** Every current pair member and the entrant must explicitly approve. */
|
||||
/** Every current group member and the entrant must explicitly approve. */
|
||||
export function answerCoalitionAdmission(seats: readonly CoalitionSeat[], value: CoalitionState | undefined,
|
||||
answer: CoalitionAdmissionAnswer): CoalitionTransition {
|
||||
check(record(answer) && id(answer.event) && id(answer.nexus) && turn(answer.turn) &&
|
||||
@@ -246,9 +249,9 @@ export function answerCoalitionAdmission(seats: readonly CoalitionSeat[], value:
|
||||
offer.approvals.every(id) && new Set(offer.approvals).size === offer.approvals.length &&
|
||||
state.usedEvents.includes(offer.event), 'The saved coalition invitation is invalid.');
|
||||
const group = groups.find(candidate => candidate.id === offer.groupId);
|
||||
check(group && group.members.length === 2 && same(group, offer.original) &&
|
||||
check(group && group.members.length < (state.variant === 'unlimited' ? 6 : 3) && same(group, offer.original) &&
|
||||
!groups.some(candidate => candidate.members.includes(offer.entrant)) &&
|
||||
seats.some(seat => seat.id === offer.entrant), 'The original pair or entrant changed.');
|
||||
seats.some(seat => seat.id === offer.entrant), 'The original group or entrant changed.');
|
||||
const participants = [...group.members, offer.entrant];
|
||||
check(participants.includes(offer.proposer) && offer.approvals.includes(offer.proposer) &&
|
||||
offer.approvals.every(actor => participants.includes(actor)) &&
|
||||
@@ -273,8 +276,8 @@ export function answerCoalitionAdmission(seats: readonly CoalitionSeat[], value:
|
||||
allyUpdates: Object.fromEntries(members.map(member => [member, null])), allySinceUpdates: {}};
|
||||
}
|
||||
|
||||
/** Provisional online policy: the departing member leaves; the remaining pair
|
||||
* stays allied, preserving that pair's original edge date and group identity. */
|
||||
/** Provisional online policy: remaining members stay allied, preserving each
|
||||
* surviving edge and group identity. A pair restores its legacy scalar fields. */
|
||||
export function leaveCoalition(seats: readonly CoalitionSeat[], value: CoalitionState | undefined,
|
||||
request: {actor: string; expectedRevision: number}): CoalitionTransition {
|
||||
check(record(request) && id(request.actor), 'Choose the departing player.');
|
||||
@@ -292,6 +295,10 @@ export function leaveCoalition(seats: readonly CoalitionSeat[], value: Coalition
|
||||
allyEntries.push([remaining[0], remaining[1]], [remaining[1], remaining[0]]);
|
||||
const retainedDates = retained.legacySince ?? [retained.formedTurn, retained.formedTurn];
|
||||
ageEntries.push([remaining[0], retainedDates[0]], [remaining[1], retainedDates[1]]);
|
||||
} else if (remaining.length >= 3) {
|
||||
const edges = group.edges.filter(edge => edge.members.every(member => remaining.includes(member))).map(copy);
|
||||
nextGroups.push({id: group.id, members: remaining, formedTurn: group.formedTurn, edges});
|
||||
for (const member of remaining) allyEntries.push([member, null]);
|
||||
} else for (const member of remaining) { allyEntries.push([member, null]); ageEntries.push([member, null]); }
|
||||
return {state: {...copy(state), revision: state.revision + 1, groups: nextGroups},
|
||||
allyUpdates: Object.fromEntries(allyEntries), allySinceUpdates: Object.fromEntries(ageEntries)};
|
||||
|
||||
Reference in new issue
Block a user