@civ-clone/base-unit-lifecycle 0.0.0-stage → 0.1.1
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/.gitattributes +4 -0
- package/.github/workflows/build-on-push.yml +39 -0
- package/.prettierrc +4 -0
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/lib/activate.d.ts +15 -0
- package/lib/activate.js +27 -0
- package/lib/activate.js.map +1 -0
- package/lib/activate.ts +47 -0
- package/lib/canStow.d.ts +8 -0
- package/lib/canStow.js +17 -0
- package/lib/canStow.js.map +1 -0
- package/lib/canStow.ts +25 -0
- package/lib/created.d.ts +9 -0
- package/lib/created.js +19 -0
- package/lib/created.js.map +1 -0
- package/lib/created.ts +24 -0
- package/lib/defeated.d.ts +25 -0
- package/lib/defeated.js +33 -0
- package/lib/defeated.js.map +1 -0
- package/lib/defeated.ts +55 -0
- package/lib/destroyed.d.ts +37 -0
- package/lib/destroyed.js +74 -0
- package/lib/destroyed.js.map +1 -0
- package/lib/destroyed.ts +106 -0
- package/lib/detonatedRules.d.ts +56 -0
- package/lib/detonatedRules.js +95 -0
- package/lib/detonatedRules.js.map +1 -0
- package/lib/detonatedRules.ts +196 -0
- package/lib/diplomatRules.d.ts +27 -0
- package/lib/diplomatRules.js +41 -0
- package/lib/diplomatRules.js.map +1 -0
- package/lib/diplomatRules.ts +83 -0
- package/lib/halted.d.ts +6 -0
- package/lib/halted.js +10 -0
- package/lib/halted.js.map +1 -0
- package/lib/halted.ts +11 -0
- package/lib/impassable.d.ts +8 -0
- package/lib/impassable.js +15 -0
- package/lib/impassable.js.map +1 -0
- package/lib/impassable.ts +19 -0
- package/lib/lifecycleRules.d.ts +118 -0
- package/lib/lifecycleRules.js +126 -0
- package/lib/lifecycleRules.js.map +1 -0
- package/lib/lifecycleRules.ts +378 -0
- package/lib/lostAtSea.d.ts +12 -0
- package/lib/lostAtSea.js +26 -0
- package/lib/lostAtSea.js.map +1 -0
- package/lib/lostAtSea.ts +37 -0
- package/lib/movedRules.d.ts +44 -0
- package/lib/movedRules.js +126 -0
- package/lib/movedRules.js.map +1 -0
- package/lib/movedRules.ts +300 -0
- package/lib/playerAction.d.ts +22 -0
- package/lib/playerAction.js +28 -0
- package/lib/playerAction.js.map +1 -0
- package/lib/playerAction.ts +64 -0
- package/lib/stowed.d.ts +2 -0
- package/lib/stowed.js +8 -0
- package/lib/stowed.js.map +1 -0
- package/lib/stowed.ts +6 -0
- package/lib/treatyPartner.d.ts +26 -0
- package/lib/treatyPartner.js +38 -0
- package/lib/treatyPartner.js.map +1 -0
- package/lib/treatyPartner.ts +78 -0
- package/lib/unsupported.d.ts +6 -0
- package/lib/unsupported.js +9 -0
- package/lib/unsupported.js.map +1 -0
- package/lib/unsupported.ts +10 -0
- package/lib/validateMove.d.ts +11 -0
- package/lib/validateMove.js +26 -0
- package/lib/validateMove.js.map +1 -0
- package/lib/validateMove.ts +36 -0
- package/package.json +61 -4
- package/tests/fixtures/civ1-unit-rules.json +89 -0
- package/tests/lifecycle.test.ts +657 -0
- package/tests/movedRules.test.ts +256 -0
- package/tests/rules.test.ts +434 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activeUnits,
|
|
3
|
+
hasActiveUnits,
|
|
4
|
+
hasInactiveUnits,
|
|
5
|
+
inactiveUnits,
|
|
6
|
+
} from '../lib/playerAction';
|
|
7
|
+
import {
|
|
8
|
+
applyVisibility,
|
|
9
|
+
emitCreated,
|
|
10
|
+
registerUnit,
|
|
11
|
+
setMoves,
|
|
12
|
+
} from '../lib/created';
|
|
13
|
+
import {
|
|
14
|
+
carriesCargo,
|
|
15
|
+
countLoss,
|
|
16
|
+
deactivate,
|
|
17
|
+
destroyedByAnother,
|
|
18
|
+
emitDestroyed,
|
|
19
|
+
isAboard,
|
|
20
|
+
leaveOwnTransport,
|
|
21
|
+
leaveTransport,
|
|
22
|
+
loseCargo,
|
|
23
|
+
notYetDestroyed,
|
|
24
|
+
removeImprovements,
|
|
25
|
+
} from '../lib/destroyed';
|
|
26
|
+
import { carriesDomain, carriesUnit } from '../lib/canStow';
|
|
27
|
+
import {
|
|
28
|
+
clearBusy,
|
|
29
|
+
hasImprovement,
|
|
30
|
+
hasMoves,
|
|
31
|
+
isBusy,
|
|
32
|
+
removeImprovement,
|
|
33
|
+
setActive,
|
|
34
|
+
} from '../lib/activate';
|
|
35
|
+
import { countLosses, emitLostAtSea, sink } from '../lib/lostAtSea';
|
|
36
|
+
import {
|
|
37
|
+
defended,
|
|
38
|
+
destroyStack,
|
|
39
|
+
emitAndDestroy,
|
|
40
|
+
hasStack,
|
|
41
|
+
notInCity,
|
|
42
|
+
} from '../lib/defeated';
|
|
43
|
+
import { fromLand, impassable, toPartnersLand } from '../lib/impassable';
|
|
44
|
+
import {
|
|
45
|
+
hasMovesFor,
|
|
46
|
+
shortOfMovesFor,
|
|
47
|
+
spendMoves,
|
|
48
|
+
spendShort,
|
|
49
|
+
} from '../lib/validateMove';
|
|
50
|
+
import Carries from '@civ-clone/base-unit-trait-carries/Carries';
|
|
51
|
+
import Destroyed from '@civ-clone/core-unit/Rules/Destroyed';
|
|
52
|
+
import Effect from '@civ-clone/core-rule/Effect';
|
|
53
|
+
import Engine from '@civ-clone/core-engine/Engine';
|
|
54
|
+
import PlayerAction from '@civ-clone/core-player/PlayerAction';
|
|
55
|
+
import RuleRegistry from '@civ-clone/core-rule/RuleRegistry';
|
|
56
|
+
import Sleep from '@civ-clone/base-unit-action-sleep/Sleep';
|
|
57
|
+
import Stowed from '@civ-clone/base-unit-action-embark/Busy/Stowed';
|
|
58
|
+
import Unit from '@civ-clone/core-unit/Unit';
|
|
59
|
+
import UnitImprovement from '@civ-clone/core-unit-improvement/UnitImprovement';
|
|
60
|
+
import { Water } from '@civ-clone/core-terrain/Types';
|
|
61
|
+
import { emitHalted } from '../lib/halted';
|
|
62
|
+
import { emitUnsupported } from '../lib/unsupported';
|
|
63
|
+
import { expect } from 'chai';
|
|
64
|
+
import { sleepAboard } from '../lib/stowed';
|
|
65
|
+
|
|
66
|
+
// Stand-ins: only the parts of a unit, a tile and the registries each function asks for.
|
|
67
|
+
|
|
68
|
+
class LandDomain extends Unit {}
|
|
69
|
+
class AirDomain extends Unit {}
|
|
70
|
+
class Sea extends Water {}
|
|
71
|
+
class Fortification extends UnitImprovement {}
|
|
72
|
+
class Veterancy extends UnitImprovement {}
|
|
73
|
+
|
|
74
|
+
const moves = (value: number) => {
|
|
75
|
+
let current = value;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
value: (): number => current,
|
|
79
|
+
set: (to: number): void => {
|
|
80
|
+
current = to;
|
|
81
|
+
},
|
|
82
|
+
subtract: (by: number): void => {
|
|
83
|
+
current -= by;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
standIn = (fields: { [key: string]: any } = {}, Type: typeof Unit = Unit) =>
|
|
88
|
+
Object.assign(Object.create(Type.prototype), fields) as Unit,
|
|
89
|
+
emitted = (engine: Engine, event: string): any[][] => {
|
|
90
|
+
const calls: any[][] = [];
|
|
91
|
+
|
|
92
|
+
engine.on(event, (...args: any[]) => calls.push(args));
|
|
93
|
+
|
|
94
|
+
return calls;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
describe('created', (): void => {
|
|
98
|
+
it('registers the unit', (): void => {
|
|
99
|
+
const registered: Unit[] = [],
|
|
100
|
+
unit = standIn();
|
|
101
|
+
|
|
102
|
+
registerUnit({ register: (u: Unit) => registered.push(u) } as any)(unit);
|
|
103
|
+
|
|
104
|
+
expect(registered).to.eql([unit]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('applies the visibility and sets full moves', (): void => {
|
|
108
|
+
let applied = 0;
|
|
109
|
+
const unitMoves = moves(0),
|
|
110
|
+
unit = standIn({
|
|
111
|
+
applyVisibility: () => applied++,
|
|
112
|
+
moves: () => unitMoves,
|
|
113
|
+
movement: () => 3,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
applyVisibility(unit);
|
|
117
|
+
setMoves(unit);
|
|
118
|
+
|
|
119
|
+
expect(applied).to.equal(1);
|
|
120
|
+
expect(unitMoves.value()).to.equal(3);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('emits unit:created', (): void => {
|
|
124
|
+
const engine = new Engine(),
|
|
125
|
+
calls = emitted(engine, 'unit:created'),
|
|
126
|
+
unit = standIn();
|
|
127
|
+
|
|
128
|
+
emitCreated(engine)(unit);
|
|
129
|
+
|
|
130
|
+
expect(calls).to.eql([[unit]]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('activate', (): void => {
|
|
135
|
+
it('asks moves and busy, and activates and clears busy', (): void => {
|
|
136
|
+
const calls: any[] = [],
|
|
137
|
+
unit = standIn({
|
|
138
|
+
moves: () => moves(1),
|
|
139
|
+
busy: () => null,
|
|
140
|
+
setActive: (...args: any[]) => calls.push(['active', ...args]),
|
|
141
|
+
setBusy: (...args: any[]) => calls.push(['busy', ...args]),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(hasMoves(unit)).to.true;
|
|
145
|
+
expect(hasMoves(standIn({ moves: () => moves(0) }))).to.false;
|
|
146
|
+
expect(isBusy(unit)).to.false;
|
|
147
|
+
expect(isBusy(standIn({ busy: () => ({}) }))).to.true;
|
|
148
|
+
|
|
149
|
+
setActive(unit);
|
|
150
|
+
clearBusy(unit);
|
|
151
|
+
|
|
152
|
+
expect(calls).to.eql([['active'], ['busy']]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('removes only the improvements of the given type', (): void => {
|
|
156
|
+
const unit = standIn(),
|
|
157
|
+
fortified = new Fortification(),
|
|
158
|
+
veteran = new Veterancy(),
|
|
159
|
+
unregistered: UnitImprovement[] = [],
|
|
160
|
+
registry = {
|
|
161
|
+
getByUnit: (u: Unit) => (u === unit ? [veteran, fortified] : []),
|
|
162
|
+
unregister: (...improvements: UnitImprovement[]) =>
|
|
163
|
+
unregistered.push(...improvements),
|
|
164
|
+
} as any;
|
|
165
|
+
|
|
166
|
+
expect(hasImprovement(registry, Fortification)(unit)).to.true;
|
|
167
|
+
expect(hasImprovement(registry, Fortification)(standIn())).to.false;
|
|
168
|
+
|
|
169
|
+
removeImprovement(registry, Fortification)(unit);
|
|
170
|
+
|
|
171
|
+
expect(unregistered).to.eql([fortified]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('validateMove', (): void => {
|
|
176
|
+
it('spends the cost when there are moves enough', (): void => {
|
|
177
|
+
const unitMoves = moves(3),
|
|
178
|
+
unit = standIn({ moves: () => unitMoves });
|
|
179
|
+
|
|
180
|
+
expect(hasMovesFor(unit, 3)).to.true;
|
|
181
|
+
expect(hasMovesFor(unit, 4)).to.false;
|
|
182
|
+
expect(shortOfMovesFor(unit, 3)).to.false;
|
|
183
|
+
expect(shortOfMovesFor(unit, 4)).to.true;
|
|
184
|
+
expect(spendMoves(unit, 2)).to.true;
|
|
185
|
+
expect(unitMoves.value()).to.equal(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('spends all that is left on a short step, and draws once to see if it gets there', (): void => {
|
|
189
|
+
const draws: number[] = [],
|
|
190
|
+
rng = (value: number) => () => {
|
|
191
|
+
draws.push(value);
|
|
192
|
+
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// 1 of 3 left, factor 0.5: gets there when 1 >= 3 * 0.5 * draw, so a draw up to 2/3.
|
|
197
|
+
[
|
|
198
|
+
[0.6, true],
|
|
199
|
+
[0.7, false],
|
|
200
|
+
].forEach(([draw, expected]) => {
|
|
201
|
+
const unitMoves = moves(1),
|
|
202
|
+
unit = standIn({ moves: () => unitMoves });
|
|
203
|
+
|
|
204
|
+
expect(spendShort(0.5, rng(draw as number))(unit, 3)).to.equal(expected);
|
|
205
|
+
expect(unitMoves.value()).to.equal(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(draws).to.eql([0.6, 0.7]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('always gets there with at least the factor of the cost left, and still draws', (): void => {
|
|
212
|
+
let draws = 0;
|
|
213
|
+
const unit = standIn({ moves: () => moves(2) });
|
|
214
|
+
|
|
215
|
+
expect(
|
|
216
|
+
spendShort(0.5, () => {
|
|
217
|
+
draws++;
|
|
218
|
+
|
|
219
|
+
return 0.999;
|
|
220
|
+
})(unit, 4)
|
|
221
|
+
).to.true;
|
|
222
|
+
expect(draws).to.equal(1);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('impassable', (): void => {
|
|
227
|
+
it('asks the step to start on land and the destination to be a partner’s', (): void => {
|
|
228
|
+
const unit = standIn(),
|
|
229
|
+
land = { isLand: () => true } as any,
|
|
230
|
+
sea = { isLand: () => false } as any,
|
|
231
|
+
partner = {} as any,
|
|
232
|
+
asked: any[] = [];
|
|
233
|
+
|
|
234
|
+
expect(fromLand(unit, sea, land)).to.true;
|
|
235
|
+
expect(fromLand(unit, land, sea)).to.false;
|
|
236
|
+
expect(
|
|
237
|
+
toPartnersLand((u, t) => {
|
|
238
|
+
asked.push([u, t]);
|
|
239
|
+
|
|
240
|
+
return partner;
|
|
241
|
+
})(unit, land)
|
|
242
|
+
).to.true;
|
|
243
|
+
expect(toPartnersLand(() => null)(unit, land)).to.false;
|
|
244
|
+
expect(asked).to.eql([[unit, land]]);
|
|
245
|
+
expect(impassable()).to.true;
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('canStow', (): void => {
|
|
250
|
+
const transportCarrying = (...carries: Carries[]) =>
|
|
251
|
+
standIn({
|
|
252
|
+
traits: (Trait: any) => (Trait === Carries ? carries : []),
|
|
253
|
+
}) as any;
|
|
254
|
+
|
|
255
|
+
it('matches only the domain the transport is declared to carry', (): void => {
|
|
256
|
+
const ship = transportCarrying(new Carries(LandDomain)),
|
|
257
|
+
land = standIn({}, LandDomain),
|
|
258
|
+
air = standIn({}, AirDomain);
|
|
259
|
+
|
|
260
|
+
expect(carriesDomain(LandDomain)(ship)).to.true;
|
|
261
|
+
expect(carriesDomain(AirDomain)(ship)).to.false;
|
|
262
|
+
expect(carriesUnit(LandDomain)(ship, land)).to.true;
|
|
263
|
+
expect(carriesUnit(LandDomain)(ship, air)).to.false;
|
|
264
|
+
expect(carriesUnit(AirDomain)(ship, air)).to.false;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('asks each Carries trait for its own domain only', (): void => {
|
|
268
|
+
const twoDomains = transportCarrying(
|
|
269
|
+
new Carries(LandDomain),
|
|
270
|
+
new Carries(AirDomain)
|
|
271
|
+
),
|
|
272
|
+
air = standIn({}, AirDomain);
|
|
273
|
+
|
|
274
|
+
expect(carriesUnit(AirDomain)(twoDomains, air)).to.true;
|
|
275
|
+
expect(carriesUnit(LandDomain)(twoDomains, air)).to.false;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('stowed', (): void => {
|
|
280
|
+
it('puts the unit to sleep where it is, and returns what action returns', (): void => {
|
|
281
|
+
const tile = {} as any,
|
|
282
|
+
performed: any[] = [],
|
|
283
|
+
unit = standIn({
|
|
284
|
+
tile: () => tile,
|
|
285
|
+
action: (action: any) => {
|
|
286
|
+
performed.push(action);
|
|
287
|
+
|
|
288
|
+
return 'result';
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(sleepAboard(unit)).to.equal('result');
|
|
293
|
+
expect(performed).to.have.length(1);
|
|
294
|
+
expect(performed[0]).to.instanceOf(Sleep);
|
|
295
|
+
expect(performed[0].from()).to.equal(tile);
|
|
296
|
+
expect(performed[0].to()).to.equal(tile);
|
|
297
|
+
expect(performed[0].unit()).to.equal(unit);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('halted and unsupported', (): void => {
|
|
302
|
+
it('emits unit:halted and unit:unsupported with their arguments', (): void => {
|
|
303
|
+
const engine = new Engine(),
|
|
304
|
+
halted = emitted(engine, 'unit:halted'),
|
|
305
|
+
unsupported = emitted(engine, 'unit:unsupported'),
|
|
306
|
+
unit = standIn(),
|
|
307
|
+
tile = {} as any,
|
|
308
|
+
city = {} as any;
|
|
309
|
+
|
|
310
|
+
emitHalted(engine)(unit, tile);
|
|
311
|
+
emitUnsupported(engine)(city, unit);
|
|
312
|
+
|
|
313
|
+
expect(halted).to.eql([[unit, tile]]);
|
|
314
|
+
expect(unsupported).to.eql([[city, unit]]);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('defeated', (): void => {
|
|
319
|
+
const setUp = () => {
|
|
320
|
+
const engine = new Engine(),
|
|
321
|
+
ruleRegistry = new RuleRegistry(),
|
|
322
|
+
destroyed: any[][] = [];
|
|
323
|
+
|
|
324
|
+
ruleRegistry.register(
|
|
325
|
+
new Destroyed(
|
|
326
|
+
new Effect((unit: Unit, player: any): void => {
|
|
327
|
+
destroyed.push([unit, player]);
|
|
328
|
+
})
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
return { engine, ruleRegistry, destroyed };
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
it('emits unit:defeated, then destroys the unit for the winner’s player', (): void => {
|
|
336
|
+
const { engine, ruleRegistry, destroyed } = setUp(),
|
|
337
|
+
calls = emitted(engine, 'unit:defeated'),
|
|
338
|
+
winner = {} as any,
|
|
339
|
+
unit = standIn(),
|
|
340
|
+
by = standIn({ player: () => winner }),
|
|
341
|
+
action = {} as any,
|
|
342
|
+
order: string[] = [];
|
|
343
|
+
|
|
344
|
+
engine.on('unit:defeated', () => order.push('emit'));
|
|
345
|
+
ruleRegistry.register(
|
|
346
|
+
new Destroyed(new Effect(() => order.push('destroy')))
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
emitAndDestroy(engine, ruleRegistry)(unit, by, action);
|
|
350
|
+
|
|
351
|
+
expect(calls).to.eql([[unit, by, action]]);
|
|
352
|
+
expect(destroyed).to.eql([[unit, winner]]);
|
|
353
|
+
expect(order).to.eql(['emit', 'destroy']);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('tells a defeated defender from a defeated attacker', (): void => {
|
|
357
|
+
const unit = standIn(),
|
|
358
|
+
by = standIn();
|
|
359
|
+
|
|
360
|
+
expect(defended(unit, by)).to.true;
|
|
361
|
+
expect(defended(unit, by, { unit: () => by } as any)).to.true;
|
|
362
|
+
expect(defended(unit, by, { unit: () => unit } as any)).to.false;
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('asks for a city on the tile', (): void => {
|
|
366
|
+
const tile = {},
|
|
367
|
+
unit = standIn({ tile: () => tile }),
|
|
368
|
+
cities = (city: any) => ({
|
|
369
|
+
getByTile: (t: any) => (t === tile ? city : 'x'),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(notInCity(cities(null) as any)(unit)).to.true;
|
|
373
|
+
expect(notInCity(cities({}) as any)(unit)).to.false;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('destroys the rest of the player’s stack, and nobody else’s', (): void => {
|
|
377
|
+
const { ruleRegistry, destroyed } = setUp(),
|
|
378
|
+
tile = {},
|
|
379
|
+
mine = {} as any,
|
|
380
|
+
theirs = {} as any,
|
|
381
|
+
winner = {} as any,
|
|
382
|
+
unit = standIn({ tile: () => tile, player: () => mine }),
|
|
383
|
+
friend = standIn({ player: () => mine }),
|
|
384
|
+
other = standIn({ player: () => theirs }),
|
|
385
|
+
units = (onTile: Unit[]) =>
|
|
386
|
+
({ getByTile: (t: any) => (t === tile ? onTile : []) } as any),
|
|
387
|
+
by = standIn({ player: () => winner });
|
|
388
|
+
|
|
389
|
+
expect(hasStack(units([unit, friend, other]))(unit)).to.true;
|
|
390
|
+
expect(hasStack(units([unit, other]))(unit)).to.false;
|
|
391
|
+
|
|
392
|
+
destroyStack(units([friend, unit, other, friend]), ruleRegistry)(unit, by);
|
|
393
|
+
|
|
394
|
+
expect(destroyed).to.eql([
|
|
395
|
+
[friend, winner],
|
|
396
|
+
[friend, winner],
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('destroyed', (): void => {
|
|
402
|
+
const transports = (manifests: any[]) => {
|
|
403
|
+
const registry = {
|
|
404
|
+
unregistered: [] as any[],
|
|
405
|
+
getByTransport: (transport: any) =>
|
|
406
|
+
manifests.filter((manifest) => manifest.transport() === transport),
|
|
407
|
+
getByUnit: (unit: any) =>
|
|
408
|
+
manifests.find((manifest) => manifest.unit() === unit),
|
|
409
|
+
hasUnit: (unit: any) =>
|
|
410
|
+
manifests.some((manifest) => manifest.unit() === unit),
|
|
411
|
+
unregister: (manifest: any) => {
|
|
412
|
+
registry.unregistered.push(manifest);
|
|
413
|
+
manifests.splice(manifests.indexOf(manifest), 1);
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
return registry;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const cargoUnit = (busy: any = null) => {
|
|
421
|
+
const unit = standIn({
|
|
422
|
+
busy: () => busy,
|
|
423
|
+
setBusy: (value: any = null) => {
|
|
424
|
+
busy = value;
|
|
425
|
+
},
|
|
426
|
+
destroyed: () => (unit as any).lost.length > 0,
|
|
427
|
+
destroy: (player: any) => (unit as any).lost.push(player),
|
|
428
|
+
lost: [] as any[],
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return unit;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
it('emits unit:destroyed and asks who destroyed it', (): void => {
|
|
435
|
+
const engine = new Engine(),
|
|
436
|
+
calls = emitted(engine, 'unit:destroyed'),
|
|
437
|
+
mine = {} as any,
|
|
438
|
+
theirs = {} as any,
|
|
439
|
+
unit = standIn({ player: () => mine });
|
|
440
|
+
|
|
441
|
+
emitDestroyed(engine)(unit, theirs);
|
|
442
|
+
|
|
443
|
+
expect(calls).to.eql([[unit, theirs]]);
|
|
444
|
+
expect(destroyedByAnother(unit, theirs)).to.true;
|
|
445
|
+
expect(destroyedByAnother(unit, mine)).to.false;
|
|
446
|
+
expect(destroyedByAnother(unit, null)).to.false;
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('counts a loss once, by type, and then deactivates', (): void => {
|
|
450
|
+
const recorded: any[] = [],
|
|
451
|
+
calls: any[] = [],
|
|
452
|
+
player = {},
|
|
453
|
+
unit = standIn({
|
|
454
|
+
player: () => player,
|
|
455
|
+
sourceClass: () => LandDomain,
|
|
456
|
+
destroyed: () => calls.includes('destroyed'),
|
|
457
|
+
setActive: (active: boolean) => calls.push(['active', active]),
|
|
458
|
+
setDestroyed: () => calls.push('destroyed'),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(notYetDestroyed(unit)).to.true;
|
|
462
|
+
|
|
463
|
+
countLoss({
|
|
464
|
+
record: (...args: any[]) => recorded.push(args),
|
|
465
|
+
} as any)(unit);
|
|
466
|
+
deactivate(unit);
|
|
467
|
+
|
|
468
|
+
expect(recorded).to.eql([[player, LandDomain]]);
|
|
469
|
+
expect(calls).to.eql([['active', false], 'destroyed']);
|
|
470
|
+
expect(notYetDestroyed(unit)).to.false;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('removes every improvement', (): void => {
|
|
474
|
+
const unit = standIn(),
|
|
475
|
+
improvements = [new Fortification(), new Veterancy()],
|
|
476
|
+
unregistered: any[] = [];
|
|
477
|
+
|
|
478
|
+
removeImprovements({
|
|
479
|
+
getByUnit: (u: Unit) => (u === unit ? improvements : []),
|
|
480
|
+
unregister: (improvement: any) => unregistered.push(improvement),
|
|
481
|
+
} as any)(unit);
|
|
482
|
+
|
|
483
|
+
expect(unregistered).to.eql(improvements);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('takes a unit off its transport and clears Stowed, and only Stowed', (): void => {
|
|
487
|
+
const stowed = cargoUnit(new Stowed()),
|
|
488
|
+
sleeping = {},
|
|
489
|
+
other = cargoUnit(sleeping),
|
|
490
|
+
ship = standIn(),
|
|
491
|
+
manifests = [
|
|
492
|
+
{ unit: () => stowed, transport: () => ship },
|
|
493
|
+
{ unit: () => other, transport: () => ship },
|
|
494
|
+
],
|
|
495
|
+
registry = transports(manifests.slice());
|
|
496
|
+
|
|
497
|
+
manifests.forEach((manifest) =>
|
|
498
|
+
leaveTransport(registry as any)(manifest as any)
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
expect(registry.unregistered).to.eql(manifests);
|
|
502
|
+
expect(stowed.busy()).to.null;
|
|
503
|
+
expect(other.busy()).to.equal(sleeping);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('loses the cargo of a transport at sea, destroyed by the same player', (): void => {
|
|
507
|
+
const player = {},
|
|
508
|
+
sea = { terrain: () => new Sea() },
|
|
509
|
+
ship = standIn({ tile: () => sea }),
|
|
510
|
+
first = cargoUnit(new Stowed()),
|
|
511
|
+
second = cargoUnit(new Stowed()),
|
|
512
|
+
registry = transports([
|
|
513
|
+
{ unit: () => first, transport: () => ship },
|
|
514
|
+
{ unit: () => second, transport: () => ship },
|
|
515
|
+
]);
|
|
516
|
+
|
|
517
|
+
expect(carriesCargo(registry as any)(ship)).to.true;
|
|
518
|
+
|
|
519
|
+
loseCargo(registry as any)(ship, player as any);
|
|
520
|
+
|
|
521
|
+
expect(registry.unregistered).to.have.length(2);
|
|
522
|
+
expect(first.busy()).to.null;
|
|
523
|
+
expect((first as any).lost).to.eql([player]);
|
|
524
|
+
expect((second as any).lost).to.eql([player]);
|
|
525
|
+
expect(carriesCargo(registry as any)(ship)).to.false;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('leaves the cargo of a transport in port, and doesn’t destroy cargo already destroyed', (): void => {
|
|
529
|
+
const port = { terrain: () => ({}) },
|
|
530
|
+
sea = { terrain: () => new Sea() },
|
|
531
|
+
inPort = standIn({ tile: () => port }),
|
|
532
|
+
atSea = standIn({ tile: () => sea }),
|
|
533
|
+
safe = cargoUnit(),
|
|
534
|
+
gone = cargoUnit(),
|
|
535
|
+
registry = transports([
|
|
536
|
+
{ unit: () => safe, transport: () => inPort },
|
|
537
|
+
{ unit: () => gone, transport: () => atSea },
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
(gone as any).lost.push('earlier');
|
|
541
|
+
|
|
542
|
+
loseCargo(registry as any)(inPort, null);
|
|
543
|
+
loseCargo(registry as any)(atSea, null);
|
|
544
|
+
|
|
545
|
+
expect((safe as any).lost).to.eql([]);
|
|
546
|
+
expect((gone as any).lost).to.eql(['earlier']);
|
|
547
|
+
expect(registry.unregistered).to.have.length(2);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('takes cargo destroyed on its own off its transport', (): void => {
|
|
551
|
+
const ship = standIn(),
|
|
552
|
+
cargo = cargoUnit(new Stowed()),
|
|
553
|
+
ashore = cargoUnit(),
|
|
554
|
+
manifest = { unit: () => cargo, transport: () => ship },
|
|
555
|
+
registry = transports([manifest]);
|
|
556
|
+
|
|
557
|
+
expect(isAboard(registry as any)(cargo)).to.true;
|
|
558
|
+
expect(isAboard(registry as any)(ashore)).to.false;
|
|
559
|
+
|
|
560
|
+
leaveOwnTransport(registry as any)(cargo);
|
|
561
|
+
|
|
562
|
+
expect(registry.unregistered).to.eql([manifest]);
|
|
563
|
+
expect(cargo.busy()).to.null;
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('lostAtSea', (): void => {
|
|
568
|
+
it('emits, counts the transport and its cargo not yet destroyed, then sinks it by no one', (): void => {
|
|
569
|
+
const engine = new Engine(),
|
|
570
|
+
calls = emitted(engine, 'unit:lost-at-sea'),
|
|
571
|
+
player = {},
|
|
572
|
+
recorded: any[] = [],
|
|
573
|
+
sunk: any[] = [],
|
|
574
|
+
ship = standIn({
|
|
575
|
+
player: () => player,
|
|
576
|
+
sourceClass: () => AirDomain,
|
|
577
|
+
destroyed: () => false,
|
|
578
|
+
destroy: (by: any) => sunk.push(by),
|
|
579
|
+
}),
|
|
580
|
+
aboard = standIn({
|
|
581
|
+
player: () => player,
|
|
582
|
+
sourceClass: () => LandDomain,
|
|
583
|
+
destroyed: () => false,
|
|
584
|
+
}),
|
|
585
|
+
gone = standIn({ destroyed: () => true }),
|
|
586
|
+
registry = {
|
|
587
|
+
getByTransport: (transport: any) =>
|
|
588
|
+
transport === ship
|
|
589
|
+
? [{ unit: () => aboard }, { unit: () => gone }]
|
|
590
|
+
: [],
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
emitLostAtSea(engine)(ship as any);
|
|
594
|
+
countLosses(
|
|
595
|
+
{ record: (...args: any[]) => recorded.push(args) } as any,
|
|
596
|
+
registry as any
|
|
597
|
+
)(ship as any);
|
|
598
|
+
sink(ship as any);
|
|
599
|
+
|
|
600
|
+
expect(calls).to.eql([[ship]]);
|
|
601
|
+
expect(recorded).to.eql([
|
|
602
|
+
[player, AirDomain],
|
|
603
|
+
[player, LandDomain],
|
|
604
|
+
]);
|
|
605
|
+
expect(sunk).to.eql([null]);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
describe('playerAction', (): void => {
|
|
610
|
+
class Active extends PlayerAction<Unit> {}
|
|
611
|
+
class Inactive extends PlayerAction<Unit> {}
|
|
612
|
+
|
|
613
|
+
const player = {} as any,
|
|
614
|
+
unit = (active: boolean, left: number, waiting = false) =>
|
|
615
|
+
standIn({
|
|
616
|
+
active: () => active,
|
|
617
|
+
moves: () => moves(left),
|
|
618
|
+
waiting: () => waiting,
|
|
619
|
+
}),
|
|
620
|
+
waitingActive = unit(true, 1, true),
|
|
621
|
+
active = unit(true, 1),
|
|
622
|
+
spent = unit(true, 0),
|
|
623
|
+
waitingAsleep = unit(false, 1, true),
|
|
624
|
+
asleep = unit(false, 1),
|
|
625
|
+
registry = (units: Unit[]) =>
|
|
626
|
+
({ getByPlayer: (p: any) => (p === player ? units : []) } as any);
|
|
627
|
+
|
|
628
|
+
it('lists active units with moves, waiting units last', (): void => {
|
|
629
|
+
const units = registry([waitingActive, spent, active, asleep]),
|
|
630
|
+
actions = activeUnits(units, Active)(player);
|
|
631
|
+
|
|
632
|
+
expect(hasActiveUnits(units)(player)).to.true;
|
|
633
|
+
expect(hasActiveUnits(registry([spent, asleep]))(player)).to.false;
|
|
634
|
+
expect(actions.map((action) => action.value())).to.eql([
|
|
635
|
+
active,
|
|
636
|
+
waitingActive,
|
|
637
|
+
]);
|
|
638
|
+
actions.forEach((action) => {
|
|
639
|
+
expect(action).to.instanceOf(Active);
|
|
640
|
+
expect(action.player()).to.equal(player);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('lists inactive units and those without moves, waiting units last', (): void => {
|
|
645
|
+
const units = registry([waitingAsleep, active, spent, asleep]),
|
|
646
|
+
actions = inactiveUnits(units, Inactive)(player);
|
|
647
|
+
|
|
648
|
+
expect(hasInactiveUnits(units)(player)).to.true;
|
|
649
|
+
expect(hasInactiveUnits(registry([active]))(player)).to.false;
|
|
650
|
+
expect(actions.map((action) => action.value())).to.eql([
|
|
651
|
+
spent,
|
|
652
|
+
asleep,
|
|
653
|
+
waitingAsleep,
|
|
654
|
+
]);
|
|
655
|
+
actions.forEach((action) => expect(action).to.instanceOf(Inactive));
|
|
656
|
+
});
|
|
657
|
+
});
|