@civ-clone/base-unit-lifecycle 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civ-clone/base-unit-lifecycle",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Building blocks for a ruleset's unit lifecycle rules: what happens when a unit is created, activated, moved, stowed aboard a transport, halted, defeated, destroyed, lost at sea or left unsupported, and which of a player's units can take orders. The ruleset keeps its own rules, ids and order, and builds their criteria and effects from these.",
5
5
  "repository": "git@github.com:civ-clone/base-unit-lifecycle.git",
6
6
  "keywords": [
@@ -29,17 +29,35 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@civ-clone/base-unit-action-embark": "^0.1.0",
32
+ "@civ-clone/base-unit-action-establish-embassy": "^0.1.0",
33
+ "@civ-clone/base-unit-action-goto": "^0.1.9",
34
+ "@civ-clone/base-unit-action-investigate-city": "^0.1.0",
35
+ "@civ-clone/base-unit-action-meet-with-leader": "^0.1.0",
36
+ "@civ-clone/base-unit-action-nuclear-attack": "^0.1.0",
32
37
  "@civ-clone/base-unit-action-rules": "^0.1.5",
33
38
  "@civ-clone/base-unit-action-sleep": "^0.1.0",
39
+ "@civ-clone/base-unit-trait-attack-ends-turn": "^0.1.0",
34
40
  "@civ-clone/base-unit-trait-carries": "^0.1.0",
41
+ "@civ-clone/base-unit-trait-coast-bound": "^0.1.0",
42
+ "@civ-clone/base-unit-trait-non-combatant": "^0.1.0",
43
+ "@civ-clone/base-unit-type-air": "^0.1.0",
44
+ "@civ-clone/base-unit-type-land": "^0.1.0",
45
+ "@civ-clone/base-unit-type-naval-transport": "^0.1.3",
35
46
  "@civ-clone/core-city": "^0.1.19",
47
+ "@civ-clone/core-city-growth": "^0.1.6",
48
+ "@civ-clone/core-diplomacy": "^0.1.8",
36
49
  "@civ-clone/core-engine": "^0.1.0",
37
50
  "@civ-clone/core-player": "^0.1.0",
51
+ "@civ-clone/core-random": "^0.1.2",
38
52
  "@civ-clone/core-rule": "^0.1.9",
39
53
  "@civ-clone/core-terrain": "^0.1.0",
54
+ "@civ-clone/core-tile-improvement": "^0.1.6",
55
+ "@civ-clone/core-turn-based-game": "^0.1.10",
40
56
  "@civ-clone/core-unit": "^0.1.31",
41
57
  "@civ-clone/core-unit-improvement": "^0.1.0",
42
58
  "@civ-clone/core-unit-transport": "^0.1.8",
43
- "@civ-clone/core-world": "^0.1.0"
59
+ "@civ-clone/core-world": "^0.1.0",
60
+ "@civ-clone/core-world-path": "^0.1.3",
61
+ "@civ-clone/library-diplomacy": "^0.1.4"
44
62
  }
45
63
  }
@@ -0,0 +1,89 @@
1
+ {
2
+ "Rules/Unit/activate": [
3
+ ["civ1-unit:unit/activate/has-moves", "Activate", 2000, 1],
4
+ ["civ1-unit:unit/activate/clear-busy", "Activate", 2000, 1],
5
+ ["civ1-unit:unit/activate/remove-improvement/Fortified", "Activate", 2000, 1]
6
+ ],
7
+ "Rules/Unit/canStow": [
8
+ ["civ1-unit:unit/can-stow/Land", "CanStow", 2000, 1],
9
+ ["civ1-unit:unit/can-stow/Air", "CanStow", 2000, 1]
10
+ ],
11
+ "Rules/Unit/created": [
12
+ ["civ1-unit:unit/created/register", "Created", 2000, 0],
13
+ ["civ1-unit:unit/created/apply-visibility", "Created", 2000, 0],
14
+ ["civ1-unit:unit/created/set-moves", "Created", 2000, 0],
15
+ ["civ1-unit:unit/created/emit", "Created", 2000, 0]
16
+ ],
17
+ "Rules/Unit/defeated": [
18
+ ["civ1-unit:unit/defeated/emit-and-destroy", "Defeated", 2000, 0],
19
+ ["civ1-unit:unit/defeated/destroy-stack", "Defeated", 2000, 3]
20
+ ],
21
+ "Rules/Unit/destroyed": [
22
+ ["civ1-unit:unit/destroyed/emit", "Destroyed", 2000, 0],
23
+ ["civ1-unit:unit/destroyed/count-loss", "Destroyed", 2000, 2],
24
+ ["civ1-unit:unit/destroyed/deactivate", "Destroyed", 2000, 0],
25
+ ["civ1-unit:unit/destroyed/remove-improvements", "Destroyed", 2000, 0],
26
+ ["civ1-unit:unit/destroyed/lose-cargo", "Destroyed", 2000, 1],
27
+ ["civ1-unit:unit/destroyed/leave-transport", "Destroyed", 2000, 1]
28
+ ],
29
+ "Rules/Unit/expectedMovementCost": [
30
+ ["civ1-unit:unit/expected-movement-cost/shortfall", "ExpectedMovementCost", 2000, 1]
31
+ ],
32
+ "Rules/Unit/halted": [
33
+ ["civ1-unit:unit/halted/emit", "Halted", 2000, 0]
34
+ ],
35
+ "Rules/Unit/impassable": [
36
+ ["civ1-unit:unit/impassable/treaty-partners-land", "Impassable", 2000, 2]
37
+ ],
38
+ "Rules/Unit/lostAtSea": [
39
+ ["civ1-unit:unit/lost-at-sea/emit", "LostAtSea", 2000, 0],
40
+ ["civ1-unit:unit/lost-at-sea/count-loss", "LostAtSea", 2000, 0],
41
+ ["civ1-unit:unit/lost-at-sea/destroy", "LostAtSea", 2000, 0]
42
+ ],
43
+ "Rules/Player/action": [
44
+ ["civ1-unit:player/action/active-units", "Action", 2000, 1],
45
+ ["civ1-unit:player/action/inactive-units", "Action", 2000, 1]
46
+ ],
47
+ "Rules/Unit/stowed": [
48
+ ["civ1-unit:unit/stowed/sleep", "Stowed", 2000, 0]
49
+ ],
50
+ "Rules/Unit/transferred": [
51
+ ["civ1-unit:unit/transferred/start-afresh", "Transferred", 2000, 0],
52
+ ["civ1-unit:unit/transferred/emit", "Transferred", 2000, 0]
53
+ ],
54
+ "Rules/Unit/unsupported": [
55
+ ["civ1-unit:unit/unsupported/emit", "Unsupported", 2000, 0]
56
+ ],
57
+ "Rules/Unit/validateMove": [
58
+ ["civ1-unit:unit/validate-move/enough-moves", "ValidateMove", 2000, 1],
59
+ ["civ1-unit:unit/validate-move/not-enough-moves", "ValidateMove", 2000, 1]
60
+ ],
61
+ "Rules/Unit/moved": [
62
+ ["civ1-unit:unit/moved/emit", "Moved", 2000, 0],
63
+ ["civ1-unit:unit/moved/apply-visibility", "Moved", 2000, 0],
64
+ ["civ1-unit:unit/moved/end-moves-when-exhausted", "Moved", 2000, 1],
65
+ ["civ1-unit:unit/moved/move-cargo", "Moved", 2000, 3],
66
+ ["civ1-unit:unit/moved/collect-aircraft", "Moved", 2000, 4],
67
+ ["civ1-unit:unit/moved/collect-sleeping", "Moved", 2000, 4],
68
+ ["civ1-unit:unit/moved/disembark", "Moved", 2000, 1],
69
+ ["civ1-unit:unit/moved/take-off", "Moved", 2000, 4],
70
+ ["civ1-unit:unit/moved/trireme-lost-at-sea", "Moved", 2000, 4],
71
+ ["civ1-unit:unit/moved/bomber/end-turn-after-attack", "Moved", 1000, 2],
72
+ ["civ1-unit:unit/moved/break-peace-treaty", "Moved", 2000, 1]
73
+ ],
74
+ "Rules/Unit/detonated": [
75
+ ["civ1-unit:unit/detonated/emit", "Detonated", 1000, 0],
76
+ ["civ1-unit:unit/detonated/destroy-units", "Detonated", 2000, 0],
77
+ ["civ1-unit:unit/detonated/halve-cities", "Detonated", 2500, 0],
78
+ ["civ1-unit:unit/detonated/fallout", "Detonated", 3000, 0]
79
+ ],
80
+ "Rules/Unit/diplomat": [
81
+ ["civ1-unit:unit/embassy-established/register", "EmbassyEstablished", 2000, 0],
82
+ ["civ1-unit:unit/city-investigated/emit", "CityInvestigated", 2000, 0],
83
+ ["civ1-unit:unit/leader-met/meet", "LeaderMet", 2000, 0],
84
+ ["civ1-unit:unit/technology-stolen/steal", "TechnologyStolen", 2000, 0],
85
+ ["civ1-unit:unit/city-sabotaged/sabotage", "CitySabotaged", 2000, 0],
86
+ ["civ1-unit:unit/revolt-incited/capture", "RevoltIncited", 2000, 0],
87
+ ["civ1-unit:unit/unit-bribed/transfer", "UnitBribed", 2000, 0]
88
+ ]
89
+ }
@@ -39,6 +39,7 @@ import {
39
39
  emitAndDestroy,
40
40
  hasStack,
41
41
  notInCity,
42
+ notSpared,
42
43
  } from '../lib/defeated';
43
44
  import { fromLand, impassable, toPartnersLand } from '../lib/impassable';
44
45
  import {
@@ -396,6 +397,25 @@ describe('defeated', (): void => {
396
397
  [friend, winner],
397
398
  ]);
398
399
  });
400
+ it('asks the ruleset whether the square spares the stack, with the square and the defender', (): void => {
401
+ const tile = {},
402
+ fort = {},
403
+ unit = standIn({ tile: () => tile }),
404
+ fortified = standIn({ tile: () => fort }),
405
+ asked: any[][] = [],
406
+ sparesStack = (square: any, defender: Unit): boolean => {
407
+ asked.push([square, defender]);
408
+
409
+ return square === fort;
410
+ };
411
+
412
+ expect(notSpared(sparesStack as any)(unit)).to.true;
413
+ expect(notSpared(sparesStack as any)(fortified)).to.false;
414
+ expect(asked).to.eql([
415
+ [tile, unit],
416
+ [fort, fortified],
417
+ ]);
418
+ });
399
419
  });
400
420
 
401
421
  describe('destroyed', (): void => {
@@ -0,0 +1,256 @@
1
+ import { Peace, Vendetta } from '@civ-clone/library-diplomacy/Declarations';
2
+ import Air from '@civ-clone/base-unit-type-air/Air';
3
+ import InteractionRegistry from '@civ-clone/core-diplomacy/InteractionRegistry';
4
+ import Land from '@civ-clone/base-unit-type-land/Land';
5
+ import Moved from '@civ-clone/core-unit/Rules/Moved';
6
+ import NavalTransport from '@civ-clone/base-unit-type-naval-transport/NavalTransport';
7
+ import Player from '@civ-clone/core-player/Player';
8
+ import RuleRegistry from '@civ-clone/core-rule/RuleRegistry';
9
+ import Sleeping from '@civ-clone/base-unit-action-sleep/Rules/Sleeping';
10
+ import Stowed from '@civ-clone/base-unit-action-embark/Busy/Stowed';
11
+ import { expect } from 'chai';
12
+ import movedRules from '../lib/movedRules';
13
+
14
+ // The bodies of `movedRules`, with stand-in units, actions and registries: each rule processed through a registry, as
15
+ // `Moved` is in a game (every rule's criteria checked before any effect runs).
16
+
17
+ class Move {
18
+ fromTile: unknown;
19
+ toTile: unknown;
20
+ forUnits: unknown[] = [];
21
+
22
+ constructor(fromTile: unknown, toTile: unknown) {
23
+ this.fromTile = fromTile;
24
+ this.toTile = toTile;
25
+ }
26
+
27
+ from() {
28
+ return this.fromTile;
29
+ }
30
+
31
+ to() {
32
+ return this.toTile;
33
+ }
34
+
35
+ forUnit(unit: unknown) {
36
+ this.forUnits.push(unit);
37
+
38
+ return this;
39
+ }
40
+ }
41
+ class Disembark extends Move {}
42
+ class Attack {}
43
+ class SneakMove extends Move {
44
+ _enemy: Player;
45
+
46
+ constructor(enemy: Player) {
47
+ super(null, null);
48
+
49
+ this._enemy = enemy;
50
+ }
51
+
52
+ enemy() {
53
+ return this._enemy;
54
+ }
55
+ }
56
+ class SneakAttack extends Attack {
57
+ _enemy: Player;
58
+
59
+ constructor(enemy: Player) {
60
+ super();
61
+
62
+ this._enemy = enemy;
63
+ }
64
+
65
+ enemy() {
66
+ return this._enemy;
67
+ }
68
+ }
69
+
70
+ const me = new Player(new RuleRegistry()),
71
+ them = new Player(new RuleRegistry());
72
+
73
+ type Stub = Record<string, unknown>;
74
+
75
+ const stub = (Type: Function, props: Stub): any =>
76
+ Object.assign(Object.create(Type.prototype), {
77
+ player: () => me,
78
+ busy: () => null,
79
+ setBusy() {},
80
+ hasTrait: () => false,
81
+ moves: () => ({ value: () => 1, set() {} }),
82
+ setActive() {},
83
+ applyVisibility() {},
84
+ action() {},
85
+ ...props,
86
+ });
87
+
88
+ const setUp = ({
89
+ cityOwner = null as Player | null,
90
+ unitsOn = [] as unknown[],
91
+ aboard = new Map<unknown, unknown>(),
92
+ interactionRegistry = new InteractionRegistry(),
93
+ } = {}) => {
94
+ const ruleRegistry = new RuleRegistry(),
95
+ unregistered: unknown[] = [];
96
+
97
+ ruleRegistry.register(
98
+ ...movedRules(
99
+ 'game',
100
+ {
101
+ cityRegistry: {
102
+ getByTile: () => (cityOwner ? { player: () => cityOwner } : null),
103
+ } as never,
104
+ engine: { emit() {} } as never,
105
+ interactionRegistry,
106
+ randomNumberGenerator: () => 1,
107
+ ruleRegistry,
108
+ transportRegistry: {
109
+ hasUnit: (unit: unknown) => aboard.has(unit),
110
+ getByUnit: (unit: unknown) => ({
111
+ transport: () => aboard.get(unit),
112
+ }),
113
+ unregister: (manifest: unknown) => unregistered.push(manifest),
114
+ } as never,
115
+ unitRegistry: { getByTile: () => unitsOn } as never,
116
+ },
117
+ {
118
+ Move: Move as never,
119
+ Disembark: Disembark as never,
120
+ attacks: [Attack, SneakAttack] as never[],
121
+ noVendetta: [SneakMove] as never[],
122
+ coastBoundLoss: 0.5,
123
+ }
124
+ )
125
+ );
126
+
127
+ return {
128
+ moved: (unit: unknown, action: unknown) =>
129
+ ruleRegistry.process(Moved, unit as never, action as never),
130
+ unregistered,
131
+ interactionRegistry,
132
+ };
133
+ };
134
+
135
+ // A transport with `cargo` aboard, recording what it unloads and stows.
136
+ const transport = (cargo: unknown[], tile: unknown = 'sea') => {
137
+ const unloaded: unknown[] = [],
138
+ stowed: unknown[] = [];
139
+
140
+ return {
141
+ unloaded,
142
+ stowed,
143
+ ship: stub(NavalTransport, {
144
+ tile: () => tile,
145
+ hasCargo: () => cargo.length > 0,
146
+ cargo: () => cargo,
147
+ unload: (unit: unknown) => unloaded.push(unit),
148
+ hasCapacity: () => true,
149
+ canStow: () => true,
150
+ stow: (unit: unknown) => stowed.push(unit),
151
+ }),
152
+ };
153
+ };
154
+
155
+ describe('movedRules', (): void => {
156
+ it('takes only sentried cargo and aircraft out of its own port, and all of it at sea', (): void => {
157
+ const awake = stub(Land, {}),
158
+ sentried = stub(Land, { busy: () => new Stowed() }),
159
+ asleep = stub(Land, { busy: () => new Sleeping() }),
160
+ aircraft = stub(Air, {}),
161
+ cargo = [awake, sentried, asleep, aircraft],
162
+ inPort = transport(cargo),
163
+ atSea = transport(cargo);
164
+
165
+ const leaving = new Move('port', 'sea');
166
+
167
+ setUp({ cityOwner: me }).moved(inPort.ship, leaving);
168
+ expect(inPort.unloaded).to.deep.equal([awake]);
169
+ expect(leaving.forUnits).to.deep.equal([sentried, asleep, aircraft]);
170
+
171
+ const sailing = new Move('sea', 'sea');
172
+
173
+ setUp().moved(atSea.ship, sailing);
174
+ expect(atSea.unloaded).to.deep.equal([]);
175
+ expect(sailing.forUnits).to.deep.equal(cargo);
176
+ });
177
+
178
+ it('collects its aircraft outside its cities, and its sleeping units in them', (): void => {
179
+ const aircraft = stub(Air, {}),
180
+ sleeper = stub(Land, { busy: () => new Sleeping() }),
181
+ awake = stub(Land, {}),
182
+ foreign = stub(Air, { player: () => them });
183
+
184
+ const atSea = transport([], 'here');
185
+
186
+ setUp({ unitsOn: [aircraft, sleeper, awake, foreign] }).moved(
187
+ atSea.ship,
188
+ new Move('there', 'here')
189
+ );
190
+ expect(atSea.stowed).to.deep.equal([aircraft]);
191
+
192
+ const inPort = transport([], 'here');
193
+
194
+ setUp({ cityOwner: me, unitsOn: [aircraft, sleeper, awake] }).moved(
195
+ inPort.ship,
196
+ new Move('there', 'here')
197
+ );
198
+ expect(inPort.stowed).to.deep.equal([sleeper]);
199
+ });
200
+
201
+ it('unloads a disembarking unit and forgets its manifest, and a unit that moves off its transport', (): void => {
202
+ const unloaded: unknown[] = [],
203
+ ship = {
204
+ unload: (unit: unknown) => unloaded.push(unit),
205
+ tile: () => 'sea',
206
+ },
207
+ leaving = stub(Land, { tile: () => 'shore' }),
208
+ takingOff = stub(Air, { tile: () => 'sky', busy: () => new Stowed() }),
209
+ aboard = new Map<unknown, unknown>([
210
+ [leaving, ship],
211
+ [takingOff, ship],
212
+ ]),
213
+ { moved, unregistered } = setUp({ aboard });
214
+
215
+ moved(leaving, new Disembark('sea', 'shore'));
216
+ expect(unloaded).to.deep.equal([leaving]);
217
+ expect(unregistered).to.length(1);
218
+
219
+ let busy: unknown = 'stowed';
220
+
221
+ takingOff.setBusy = (value?: unknown) => (busy = value ?? null);
222
+ moved(takingOff, new Move('sea', 'sky'));
223
+ expect(unloaded).to.deep.equal([leaving, takingOff]);
224
+ expect(busy).to.null;
225
+ });
226
+
227
+ it('ends a treaty broken by a sneak attack, with a grudge, and one broken by a sneak move, without', (): void => {
228
+ const attacker = stub(Land, {}),
229
+ attacked = setUp(),
230
+ walked = setUp();
231
+
232
+ attacked.interactionRegistry.register(
233
+ new Peace(me, them, new RuleRegistry())
234
+ );
235
+ walked.interactionRegistry.register(
236
+ new Peace(me, them, new RuleRegistry())
237
+ );
238
+
239
+ attacked.moved(attacker, new SneakAttack(them));
240
+ walked.moved(attacker, new SneakMove(them));
241
+
242
+ const active = (registry: InteractionRegistry, Type: Function) =>
243
+ registry
244
+ .entries()
245
+ .filter(
246
+ (interaction) =>
247
+ interaction instanceof Type &&
248
+ (interaction as unknown as { active(): boolean }).active()
249
+ ).length;
250
+
251
+ expect(active(attacked.interactionRegistry, Peace)).to.equal(0);
252
+ expect(active(attacked.interactionRegistry, Vendetta)).to.equal(1);
253
+ expect(active(walked.interactionRegistry, Peace)).to.equal(0);
254
+ expect(active(walked.interactionRegistry, Vendetta)).to.equal(0);
255
+ });
256
+ });