@civ-clone/base-unit-fuel 0.0.0-stage → 0.1.0

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,45 @@
1
1
  {
2
2
  "name": "@civ-clone/base-unit-fuel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Fuel for units that must return to base: a range, the turns a unit has spent out, the places it refuels, and running out of fuel.",
5
+ "repository": "git@github.com:civ-clone/base-unit-fuel.git",
6
+ "keywords": [
7
+ "typescript",
8
+ "civilization"
9
+ ],
10
+ "author": "dom111 <dom111@users.noreply.github.com>",
11
+ "license": "MIT",
12
+ "private": false,
13
+ "scripts": {
14
+ "build": "npm run ts:compile && npm run prettier:format",
15
+ "prettier:check": "prettier --config .prettierrc --check '**/*.ts'",
16
+ "prettier:format": "prettier --config .prettierrc '**/*.ts' --write",
17
+ "test": "ts-mocha ./tests/*.test.ts",
18
+ "ts:compile": "tsc --build tsconfig.json"
19
+ },
20
+ "devDependencies": {
21
+ "@civ-clone/core-game": "^0.1.17",
22
+ "@civ-clone/core-save-game": "^0.1.23",
23
+ "@civ-clone/core-terrain": "^0.1.0",
24
+ "@civ-clone/core-world-generator": "^0.1.0",
25
+ "@types/chai": "^4.0.0",
26
+ "@types/mocha": "^10.0.0",
27
+ "@types/node": "^16.0.0",
28
+ "chai": "^4.0.0",
29
+ "mocha": "^10.0.0",
30
+ "prettier": "^2.8.8",
31
+ "ts-mocha": "^10.0.0",
32
+ "typescript": "^4.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@civ-clone/core-city": "^0.1.19",
36
+ "@civ-clone/core-player": "^0.1.10",
37
+ "@civ-clone/core-rule": "^0.1.9",
38
+ "@civ-clone/core-tile-improvement": "^0.1.6",
39
+ "@civ-clone/core-unit": "^0.1.32",
40
+ "@civ-clone/core-unit-improvement": "^0.1.0",
41
+ "@civ-clone/core-unit-transport": "^0.1.10",
42
+ "@civ-clone/core-world": "^0.1.24",
43
+ "@civ-clone/core-yield": "^0.1.0"
44
+ }
45
+ }
@@ -0,0 +1,460 @@
1
+ import { OutOfFuel, Refuel } from '../lib/Rules';
2
+ import fuelRules, { CountOn, refuels, turnsAloft } from '../lib/fuelRules';
3
+ import { fuelState, refuelInReach } from '../lib/reach';
4
+ import {
5
+ refuelAboard,
6
+ refuelInOwnCity,
7
+ refuelInStackWith,
8
+ refuelOnTileImprovement,
9
+ } from '../lib/refuel';
10
+ import Action from '@civ-clone/core-unit/Action';
11
+ import Aloft from '../lib/Aloft';
12
+ import City from '@civ-clone/core-city/City';
13
+ import CityRegistry from '@civ-clone/core-city/CityRegistry';
14
+ import Criterion from '@civ-clone/core-rule/Criterion';
15
+ import Destroyed from '@civ-clone/core-unit/Rules/Destroyed';
16
+ import Effect from '@civ-clone/core-rule/Effect';
17
+ import Fuel from '../lib/Fuel';
18
+ import Generator from '@civ-clone/core-world-generator/Generator';
19
+ import { Movement } from '@civ-clone/core-unit/Yields';
20
+ import Moved from '@civ-clone/core-unit/Rules/Moved';
21
+ import Player from '@civ-clone/core-player/Player';
22
+ import RuleRegistry from '@civ-clone/core-rule/RuleRegistry';
23
+ import Terrain from '@civ-clone/core-terrain/Terrain';
24
+ import Tile from '@civ-clone/core-world/Tile';
25
+ import TileImprovement from '@civ-clone/core-tile-improvement/TileImprovement';
26
+ import TileImprovementRegistry from '@civ-clone/core-tile-improvement/TileImprovementRegistry';
27
+ import TransportManifest from '@civ-clone/core-unit-transport/TransportManifest';
28
+ import TransportRegistry from '@civ-clone/core-unit-transport/TransportRegistry';
29
+ import TurnEnd from '@civ-clone/core-player/Rules/TurnEnd';
30
+ import Unit from '@civ-clone/core-unit/Unit';
31
+ import UnitImprovementRegistry from '@civ-clone/core-unit-improvement/UnitImprovementRegistry';
32
+ import UnitRegistry from '@civ-clone/core-unit/UnitRegistry';
33
+ import World from '@civ-clone/core-world/World';
34
+ import { Yield as YieldRule } from '@civ-clone/core-unit/Rules/Yield';
35
+ import YieldValue from '@civ-clone/core-yield/Yield';
36
+ import { expect } from 'chai';
37
+
38
+ // OpenCiv2 (github.com/richardpenman/civ2, read at 49f26ec), `unit_move_dir_air_fuel`: a unit refuels in a city, on an
39
+ // airbase or in a stack with a unit that carries it; each time it is left out its count goes up, and it is lost once
40
+ // the count reaches its range. A range of 1 must refuel every turn; 2 can stay out one turn. Counted here at the end
41
+ // of each turn, as civ1-unit's fuel rule counts it.
42
+
43
+ class Airfield extends TileImprovement {}
44
+
45
+ class Step extends Action {}
46
+
47
+ const setUp = (countOn: CountOn = 'turn-end') => {
48
+ const ruleRegistry = new RuleRegistry(),
49
+ unitRegistry = new UnitRegistry(),
50
+ cityRegistry = new CityRegistry(),
51
+ unitImprovementRegistry = new UnitImprovementRegistry(),
52
+ tileImprovementRegistry = new TileImprovementRegistry(),
53
+ transportRegistry = new TransportRegistry(),
54
+ world = new World(new Generator(1, 10)),
55
+ tiles = Array.from(
56
+ { length: 10 },
57
+ (_, x: number): Tile => new Tile(x, 0, new Terrain(), world)
58
+ ),
59
+ player = new Player(),
60
+ ranges = new Map<Unit, number>(),
61
+ events: [string, Unit][] = [],
62
+ unit = (
63
+ tile: Tile,
64
+ range: number = 0,
65
+ owner: Player = player,
66
+ movement: number = 10
67
+ ): Unit => {
68
+ const created = new Unit(null, owner, tile, ruleRegistry);
69
+
70
+ ranges.set(created, range);
71
+ unitRegistry.register(created);
72
+ created.moves().set(movement);
73
+
74
+ return created;
75
+ },
76
+ city = (tile: Tile, owner: Player = player): City => {
77
+ const created = new City(owner, tile, 'City', ruleRegistry);
78
+
79
+ cityRegistry.register(created);
80
+
81
+ return created;
82
+ };
83
+
84
+ ruleRegistry.register(
85
+ new YieldRule(
86
+ new Criterion((unit: Unit): boolean => ranges.has(unit)),
87
+ new Effect((unit: Unit, unitYield: YieldValue): void => {
88
+ if (unitYield instanceof Fuel) {
89
+ unitYield.set(ranges.get(unit)!);
90
+ }
91
+
92
+ if (unitYield instanceof Movement) {
93
+ unitYield.set(10);
94
+ }
95
+ })
96
+ ),
97
+ new Destroyed(
98
+ new Effect((destroyed: Unit): void => {
99
+ events.push(['destroyed', destroyed]);
100
+ destroyed.setDestroyed();
101
+ })
102
+ ),
103
+ new OutOfFuel(
104
+ new Effect((lost: Unit): void => {
105
+ events.push(['out-of-fuel', lost]);
106
+ })
107
+ ),
108
+ ...fuelRules(
109
+ { ruleRegistry, unitImprovementRegistry, unitRegistry },
110
+ { prefix: 'test', countOn }
111
+ ),
112
+ refuelInOwnCity(cityRegistry),
113
+ refuelOnTileImprovement(Airfield, tileImprovementRegistry)
114
+ );
115
+
116
+ const endTurn = (forPlayer: Player = player): void =>
117
+ ruleRegistry.process(TurnEnd, forPlayer),
118
+ // Moves `moving` to `to` (or attacks `to`, staying where it is), leaving it `movesLeft`, as a move action does
119
+ // before processing `Moved`.
120
+ move = (
121
+ moving: Unit,
122
+ to: Tile,
123
+ movesLeft: number,
124
+ attack: boolean = false
125
+ ): void => {
126
+ const from = moving.tile();
127
+
128
+ if (!attack) {
129
+ moving.setTile(to);
130
+ }
131
+
132
+ moving.moves().set(movesLeft);
133
+ ruleRegistry.process(
134
+ Moved,
135
+ moving,
136
+ new Step(from, to, moving, ruleRegistry)
137
+ );
138
+ };
139
+
140
+ return {
141
+ city,
142
+ endTurn,
143
+ events,
144
+ move,
145
+ player,
146
+ ruleRegistry,
147
+ tileImprovementRegistry,
148
+ tiles,
149
+ transportRegistry,
150
+ unit,
151
+ unitImprovementRegistry,
152
+ unitRegistry,
153
+ };
154
+ };
155
+
156
+ describe('fuelRules', (): void => {
157
+ it('should lose a range-1 unit that ends its turn away from a refuelling point', (): void => {
158
+ const { endTurn, events, tiles, unit } = setUp(),
159
+ out = unit(tiles[3], 1);
160
+
161
+ endTurn();
162
+
163
+ expect(events).to.deep.equal([
164
+ ['out-of-fuel', out],
165
+ ['destroyed', out],
166
+ ]);
167
+ });
168
+
169
+ it('should let a range-2 unit stay out one turn, counting it, and lose it at the second', (): void => {
170
+ const { endTurn, events, tiles, unit, unitImprovementRegistry } = setUp(),
171
+ out = unit(tiles[3], 2);
172
+
173
+ endTurn();
174
+
175
+ expect(events).to.deep.equal([]);
176
+ expect(turnsAloft(unitImprovementRegistry, out)).to.equal(1);
177
+
178
+ endTurn();
179
+
180
+ expect(events.map(([name]) => name)).to.deep.equal([
181
+ 'out-of-fuel',
182
+ 'destroyed',
183
+ ]);
184
+ expect(unitImprovementRegistry.getByUnit(out)).to.deep.equal([]);
185
+ });
186
+
187
+ it('should refuel in an own city or on the improvement, clearing the count', (): void => {
188
+ const {
189
+ city,
190
+ endTurn,
191
+ events,
192
+ tileImprovementRegistry,
193
+ tiles,
194
+ unit,
195
+ unitImprovementRegistry,
196
+ } = setUp(),
197
+ inCity = unit(tiles[0], 1),
198
+ onAirfield = unit(tiles[3], 2);
199
+
200
+ city(tiles[0]);
201
+ endTurn();
202
+
203
+ expect(turnsAloft(unitImprovementRegistry, onAirfield)).to.equal(1);
204
+
205
+ tileImprovementRegistry.register(new Airfield(tiles[3]));
206
+ endTurn();
207
+
208
+ expect(events).to.deep.equal([]);
209
+ expect(turnsAloft(unitImprovementRegistry, inCity)).to.equal(0);
210
+ expect(unitImprovementRegistry.getByUnit(onAirfield)).to.deep.equal([]);
211
+ });
212
+
213
+ it("should not refuel in another player's city", (): void => {
214
+ const { city, endTurn, events, tiles, unit } = setUp(),
215
+ out = unit(tiles[0], 1);
216
+
217
+ city(tiles[0], new Player());
218
+ endTurn();
219
+
220
+ expect(events[0]).to.deep.equal(['out-of-fuel', out]);
221
+ });
222
+
223
+ it("should leave units with no range, and other players' units, alone", (): void => {
224
+ const { endTurn, events, tiles, unit, unitImprovementRegistry } = setUp(),
225
+ grounded = unit(tiles[3]),
226
+ foreign = unit(tiles[3], 1, new Player());
227
+
228
+ endTurn();
229
+
230
+ expect(events).to.deep.equal([]);
231
+ expect(unitImprovementRegistry.getByUnit(grounded)).to.deep.equal([]);
232
+ expect(foreign.destroyed()).to.equal(false);
233
+ });
234
+
235
+ it('should keep the count on a saved field of the mark only', (): void => {
236
+ const { endTurn, tiles, unit, unitImprovementRegistry } = setUp(),
237
+ out = unit(tiles[3], 3);
238
+
239
+ endTurn();
240
+ endTurn();
241
+
242
+ const [aloft] = unitImprovementRegistry.getByUnit(out) as Aloft[];
243
+
244
+ expect(aloft).to.be.instanceOf(Aloft);
245
+ expect(aloft.turns()).to.equal(2);
246
+ expect(Object.keys(out)).not.to.include('_turns');
247
+ });
248
+
249
+ it('should name its rules with the prefix', (): void => {
250
+ expect(
251
+ fuelRules({}, { prefix: 'test' }).map((rule) => rule.id())
252
+ ).to.deep.equal([
253
+ 'test:player/turn-end/fuel',
254
+ 'test:unit/out-of-fuel/destroy',
255
+ ]);
256
+ });
257
+ });
258
+
259
+ describe('refuel rules', (): void => {
260
+ it('should refuel in a stack with a base, only for the units it applies to', (): void => {
261
+ const { player, ruleRegistry, tiles, unit, unitRegistry } = setUp(),
262
+ mothership = unit(tiles[5]),
263
+ plane = unit(tiles[5], 1),
264
+ other = unit(tiles[5], 1),
265
+ bases = new Set<Unit>([mothership]);
266
+
267
+ ruleRegistry.register(
268
+ refuelInStackWith(
269
+ (base: Unit): boolean => bases.has(base),
270
+ unitRegistry,
271
+ (refuelling: Unit): boolean => refuelling !== other
272
+ )
273
+ );
274
+
275
+ expect(refuels(ruleRegistry, plane)).to.equal(true);
276
+ expect(refuels(ruleRegistry, other)).to.equal(false);
277
+ expect(refuels(ruleRegistry, mothership)).to.equal(false);
278
+ expect(player).to.be.instanceOf(Player);
279
+ });
280
+
281
+ it('should refuel aboard a transport', (): void => {
282
+ const { ruleRegistry, tiles, transportRegistry, unit } = setUp(),
283
+ transport = unit(tiles[5]),
284
+ plane = unit(tiles[5], 1);
285
+
286
+ ruleRegistry.register(refuelAboard(transportRegistry));
287
+
288
+ expect(refuels(ruleRegistry, plane)).to.equal(false);
289
+
290
+ transportRegistry.register(
291
+ new TransportManifest(transport as any, plane, tiles[5])
292
+ );
293
+
294
+ expect(refuels(ruleRegistry, plane)).to.equal(true);
295
+ });
296
+
297
+ it('should ignore a rule whose effect does not answer true', (): void => {
298
+ const { ruleRegistry, tiles, unit } = setUp(),
299
+ plane = unit(tiles[5], 1);
300
+
301
+ ruleRegistry.register(new Refuel(new Effect((): boolean => false)));
302
+
303
+ expect(refuels(ruleRegistry, plane)).to.equal(false);
304
+ });
305
+ });
306
+
307
+ describe('reach', (): void => {
308
+ it('should give the turns left and the moves before the last of them', (): void => {
309
+ const { endTurn, tiles, unit, unitImprovementRegistry } = setUp(),
310
+ out = unit(tiles[3], 2, undefined, 4);
311
+
312
+ expect(fuelState(unitImprovementRegistry, out)).to.deep.equal({
313
+ range: 2,
314
+ turnsAloft: 0,
315
+ turnsLeft: 2,
316
+ reach: 14,
317
+ });
318
+
319
+ endTurn();
320
+
321
+ expect(fuelState(unitImprovementRegistry, out)).to.deep.equal({
322
+ range: 2,
323
+ turnsAloft: 1,
324
+ turnsLeft: 1,
325
+ reach: 4,
326
+ });
327
+ expect(fuelState(unitImprovementRegistry, unit(tiles[3]))).to.equal(null);
328
+ });
329
+
330
+ it('should find the nearest refuelling point within reach', (): void => {
331
+ const {
332
+ city,
333
+ endTurn,
334
+ ruleRegistry,
335
+ tiles,
336
+ unit,
337
+ unitImprovementRegistry,
338
+ } = setUp(),
339
+ out = unit(tiles[5], 2, undefined, 2),
340
+ options = {
341
+ distance: (from: Tile, to: Tile): number => Math.abs(from.x() - to.x()),
342
+ candidates: (): Tile[] => tiles,
343
+ };
344
+
345
+ city(tiles[0]);
346
+ city(tiles[9]);
347
+
348
+ // 2 moves left and 10 more next turn: both cities, 4 and 5 away; the nearer.
349
+ expect(
350
+ refuelInReach(ruleRegistry, unitImprovementRegistry, out, options)
351
+ ).to.equal(tiles[9]);
352
+
353
+ endTurn();
354
+ out.moves().set(3);
355
+
356
+ // Out a turn already, with 3 moves: neither.
357
+ expect(
358
+ refuelInReach(ruleRegistry, unitImprovementRegistry, out, options)
359
+ ).to.equal(null);
360
+
361
+ out.moves().set(4);
362
+
363
+ expect(
364
+ refuelInReach(ruleRegistry, unitImprovementRegistry, out, options)
365
+ ).to.equal(tiles[9]);
366
+ });
367
+ });
368
+
369
+ describe('fuelRules, counting when a unit runs out of moves (`unit_move_dir_air_fuel`)', (): void => {
370
+ it('should count a turn out only when a move leaves the unit with no moves, and not at the end of the turn', (): void => {
371
+ const { endTurn, events, move, tiles, unit, unitImprovementRegistry } =
372
+ setUp('out-of-moves'),
373
+ out = unit(tiles[0], 2);
374
+
375
+ move(out, tiles[1], 5);
376
+ endTurn();
377
+
378
+ // Moves left, and no count at the end of the turn: the original's rule.
379
+ expect(turnsAloft(unitImprovementRegistry, out)).to.equal(0);
380
+
381
+ move(out, tiles[2], 0);
382
+
383
+ expect(turnsAloft(unitImprovementRegistry, out)).to.equal(1);
384
+ expect(events).to.deep.equal([]);
385
+
386
+ move(out, tiles[3], 0);
387
+
388
+ expect(events).to.deep.equal([
389
+ ['out-of-fuel', out],
390
+ ['destroyed', out],
391
+ ]);
392
+ });
393
+
394
+ it('should lose a range-1 unit as soon as it runs out of moves away from a refuelling point', (): void => {
395
+ const { events, move, tiles, unit } = setUp('out-of-moves'),
396
+ out = unit(tiles[0], 1);
397
+
398
+ move(out, tiles[1], 0);
399
+
400
+ expect(events[0]).to.deep.equal(['out-of-fuel', out]);
401
+ });
402
+
403
+ it('should land where it refuels: the count goes, and so do its moves', (): void => {
404
+ const { city, move, tiles, unit, unitImprovementRegistry } =
405
+ setUp('out-of-moves'),
406
+ out = unit(tiles[2], 2);
407
+
408
+ city(tiles[0]);
409
+ move(out, tiles[3], 0);
410
+
411
+ expect(turnsAloft(unitImprovementRegistry, out)).to.equal(1);
412
+
413
+ move(out, tiles[0], 4);
414
+
415
+ expect(turnsAloft(unitImprovementRegistry, out)).to.equal(0);
416
+ expect(out.moves().value()).to.equal(0);
417
+ });
418
+
419
+ it('should not land a range-1 unit that attacked with moves to spare, but land one that attacked with none', (): void => {
420
+ const { city, events, move, tiles, unit } = setUp('out-of-moves'),
421
+ fresh = unit(tiles[0], 1),
422
+ spent = unit(tiles[0], 1),
423
+ longer = unit(tiles[0], 2);
424
+
425
+ city(tiles[0]);
426
+
427
+ // Attacking the next square from its city, staying there.
428
+ move(fresh, tiles[1], 4, true);
429
+ move(longer, tiles[1], 4, true);
430
+
431
+ expect(fresh.moves().value()).to.equal(4);
432
+ expect(longer.moves().value()).to.equal(0);
433
+
434
+ move(spent, tiles[1], 0, true);
435
+
436
+ expect(spent.moves().value()).to.equal(0);
437
+ expect(events).to.deep.equal([]);
438
+ });
439
+
440
+ it('should ignore an action on its own square, and units with no range', (): void => {
441
+ const { events, move, tiles, unit, unitImprovementRegistry } =
442
+ setUp('out-of-moves'),
443
+ out = unit(tiles[3], 1),
444
+ grounded = unit(tiles[3]);
445
+
446
+ move(out, tiles[3], 0);
447
+ move(grounded, tiles[4], 0);
448
+
449
+ expect(events).to.deep.equal([]);
450
+ expect(unitImprovementRegistry.getByUnit(grounded)).to.deep.equal([]);
451
+ });
452
+
453
+ it('should name its counting rule for moves', (): void => {
454
+ expect(
455
+ fuelRules({}, { prefix: 'test', countOn: 'out-of-moves' }).map((rule) =>
456
+ rule.id()
457
+ )
458
+ ).to.deep.equal(['test:unit/moved/fuel', 'test:unit/out-of-fuel/destroy']);
459
+ });
460
+ });
@@ -0,0 +1,54 @@
1
+ import Aloft from '../lib/Aloft';
2
+ import { Game } from '@civ-clone/core-game/Game';
3
+ import Player from '@civ-clone/core-player/Player';
4
+ import Tile from '@civ-clone/core-world/Tile';
5
+ import Unit from '@civ-clone/core-unit/Unit';
6
+ import { expect } from 'chai';
7
+ import { gameForLoad } from '@civ-clone/core-save-game/gameForLoad';
8
+ import { hydrate } from '@civ-clone/core-save-game/hydrate';
9
+ import { registerClasses } from '@civ-clone/core-save-game/registerClasses';
10
+ import { save } from '@civ-clone/core-save-game/save';
11
+
12
+ class Flyer extends Unit {}
13
+
14
+ describe('saving `Aloft`', (): void => {
15
+ it('should round-trip the count through core-save-game, once the game offers the class', (): void => {
16
+ const game = new Game(),
17
+ player = new Player(game.rules),
18
+ out = new Flyer(null, player, null as unknown as Tile, game.rules),
19
+ home = new Flyer(null, player, null as unknown as Tile, game.rules);
20
+
21
+ game.engine.registerPlugins({ '@civ-clone/base-unit-fuel': 'test' });
22
+ // What a ruleset does: offer the class, as it offers `Veteran`.
23
+ game.availableUnitImprovements.register(Aloft);
24
+ registerClasses(game);
25
+ game.classes.register(Flyer);
26
+ game.units.register(out, home);
27
+ game.unitImprovements.register(new Aloft(out, 2));
28
+
29
+ const file = save(game, { name: 'test', createdAt: 0 }),
30
+ loaded = gameForLoad({
31
+ availableUnitImprovements: game.availableUnitImprovements,
32
+ classes: game.classes,
33
+ engine: game.engine,
34
+ rules: game.rules,
35
+ });
36
+
37
+ hydrate(file, loaded);
38
+
39
+ const [loadedOut, loadedHome] = loaded.units.entries(),
40
+ [aloft] = loaded.unitImprovements.getByUnit(loadedOut) as Aloft[];
41
+
42
+ expect(aloft).to.be.instanceOf(Aloft);
43
+ expect(aloft.turns()).to.equal(2);
44
+ expect(aloft.unit()).to.equal(loadedOut);
45
+ expect(loaded.unitImprovements.getByUnit(loadedHome)).to.deep.equal([]);
46
+ // Saved again, the entities are the same (the id counters move with each new `Game`).
47
+ expect(
48
+ JSON.stringify(save(loaded, { name: 'test', createdAt: 0 }).entities)
49
+ ).to.equal(JSON.stringify(file.entities));
50
+ expect(
51
+ file.entities.find(({ id }) => id === aloft.id())!.state
52
+ ).to.have.property('_turns', 2);
53
+ });
54
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "lib": [
5
+ "es2020"
6
+ ],
7
+ "module": "commonjs",
8
+ "sourceMap": true,
9
+ "strict": true,
10
+ "target": "ES2018"
11
+ },
12
+ "exclude": [
13
+ "node_modules",
14
+ "tests"
15
+ ]
16
+ }