No man's land: TurnResult.unowned + release/re-claim in applyTurnResult + distinct grey map rendering (#16 P3)
- shared/protocol.ts: TurnResult.unowned (keys released this turn) + the map presentation contract (regionFillStyle / unownedTooltipLabel, hue-less grey) - shared/types.ts: required GameState.unowned (cumulative mid-game releases; [] at init, distinct from preset-unassigned regions) - server/engine.ts: applyTurnResult applies releases first (sole-owner only, never fabricating no man's land from unassigned keys), then claims against the post-release snapshot (gain on an ownerless key = re-claim); lone release is isMeaningful; prompts + user payload carry the unowned field - server/stub.ts: unchanged behavior (never releases; e2e stays stable) - server/db.ts: legacy rows load with unowned defaulted to [] - server/index.ts: create-game sets []; jump reference table includes released keys so the model can name them for re-claim - WorldMap/App: unowned prop -> visible grey + 'No man's land' tooltip - unit tests: 105 -> 127 (release/re-claim suites, presentation contract, legacy-row normalization)
This commit is contained in:
+69
-1
@@ -1,6 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildRegionMap } from './map';
|
||||
import { mapFeatureOwner, nationColor, regionOwner, type RegionMap } from '../shared/protocol';
|
||||
import {
|
||||
mapFeatureOwner,
|
||||
nationColor,
|
||||
NO_MANS_LAND_FILL,
|
||||
NO_MANS_LAND_STROKE,
|
||||
OWNED_STROKE,
|
||||
regionFillStyle,
|
||||
regionOwner,
|
||||
UNOWNED_FILL,
|
||||
UNOWNED_STROKE,
|
||||
unownedTooltipLabel,
|
||||
type RegionMap,
|
||||
} from '../shared/protocol';
|
||||
import type { GameState } from '../shared/types';
|
||||
|
||||
// Region-key fixtures (v0.19, #16 P1): ownership and the RegionMap are now keyed
|
||||
@@ -35,6 +47,7 @@ const state: GameState = {
|
||||
events: [],
|
||||
pending: [],
|
||||
version: 1,
|
||||
unowned: [],
|
||||
};
|
||||
|
||||
// The RegionMap built from the fixture: the authoritative region key → owner source.
|
||||
@@ -49,6 +62,16 @@ describe('buildRegionMap', () => {
|
||||
// a region nobody owns is simply absent
|
||||
expect(m['normandie-dddd']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('(v0.22 #16 P3) a released (no-man\'s-land) key — in GameState.unowned, owned by no nation — is absent: absence IS unowned', () => {
|
||||
const s: GameState = JSON.parse(JSON.stringify(state));
|
||||
// simulate a mid-game release: the key left every territory and entered unowned
|
||||
s.nations.uk.territory = s.nations.uk.territory.filter((k) => k !== 'albania-aaaa');
|
||||
s.unowned = ['albania-aaaa'];
|
||||
const m = buildRegionMap(s);
|
||||
expect(m['albania-aaaa']).toBeUndefined(); // the map cannot tell which kind of unowned
|
||||
expect(s.unowned).toEqual(['albania-aaaa']); // ...the client's GameState.unowned decides that
|
||||
});
|
||||
});
|
||||
|
||||
describe('regionOwner', () => {
|
||||
@@ -143,3 +166,48 @@ describe('nationColor', () => {
|
||||
expect(nationColor('uk')).toMatch(/^hsl\(\d+ 58% 46%\)$/);
|
||||
});
|
||||
});
|
||||
|
||||
// (v0.22, #16 P3) The no-man's-land presentation contract lives in
|
||||
// shared/protocol (shared/*.test.ts is not in the vitest glob, so these
|
||||
// tests live here): the distinct visible grey, the faint preset-unassigned
|
||||
// look, the per-kind borders, and the tooltip sub-line.
|
||||
describe('no man\'s land presentation contract (v0.22 #16 P3)', () => {
|
||||
it('regionFillStyle: an owned region uses its nation color + the owned border', () => {
|
||||
expect(regionFillStyle(regions['albania-aaaa'], false)).toEqual({
|
||||
fill: nationColor('uk'),
|
||||
stroke: OWNED_STROKE,
|
||||
});
|
||||
expect(regionFillStyle(regions['brandenburg-cccc'], true)).toEqual({
|
||||
fill: nationColor('de'),
|
||||
stroke: OWNED_STROKE,
|
||||
}); // the no-man's-land flag never affects an owned region
|
||||
});
|
||||
|
||||
it('regionFillStyle: an unowned region that IS no man\'s land gets the distinct visible grey', () => {
|
||||
expect(regionFillStyle(undefined, true)).toEqual({
|
||||
fill: NO_MANS_LAND_FILL,
|
||||
stroke: NO_MANS_LAND_STROKE,
|
||||
});
|
||||
});
|
||||
|
||||
it('regionFillStyle: an unowned region that was never released keeps the faint look', () => {
|
||||
expect(regionFillStyle(undefined, false)).toEqual({
|
||||
fill: UNOWNED_FILL,
|
||||
stroke: UNOWNED_STROKE,
|
||||
});
|
||||
});
|
||||
|
||||
it('the no-man\'s-land grey can never collide with a nation color (hue-less vs HSL)', () => {
|
||||
expect(NO_MANS_LAND_FILL).not.toMatch(/^hsl\(/);
|
||||
expect(NO_MANS_LAND_FILL).not.toBe(nationColor('uk'));
|
||||
expect(NO_MANS_LAND_FILL).not.toBe(nationColor('de'));
|
||||
// and it must be visibly different from the faint preset-unassigned fill
|
||||
expect(NO_MANS_LAND_FILL).not.toBe(UNOWNED_FILL);
|
||||
expect(NO_MANS_LAND_STROKE).not.toBe(UNOWNED_STROKE);
|
||||
});
|
||||
|
||||
it('unownedTooltipLabel: the tooltip sub-line names the two kinds of unowned', () => {
|
||||
expect(unownedTooltipLabel(true)).toBe("No man's land");
|
||||
expect(unownedTooltipLabel(false)).toBe('No owner');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user