@thegraid/hexlib 1.0.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.
@@ -0,0 +1,69 @@
1
+ import { stime } from "@thegraid/common-lib";
2
+ /** Simple async Image loader [from ImageReveal.loadImage()]
3
+ *
4
+ * see also: createjs.ImageLoader, which we don't use.
5
+ */
6
+ export class ImageLoader {
7
+ static ipser = 0; // debug
8
+ /**
9
+ * Promise to load url as HTMLImageElement
10
+ */
11
+ loadImage(fname0, ext = this.ext) {
12
+ const fname = fname0.split('.')[0];
13
+ const ip0 = this.ipmap.get(fname);
14
+ if (ip0) {
15
+ return ip0;
16
+ }
17
+ const url = `${this.root}${fname}.${ext}`;
18
+ //console.log(stime(`image-loader: try loadImage`), url)
19
+ const ip = new Promise((res, rej) => {
20
+ const img = new Image();
21
+ img.onload = (evt => {
22
+ img.Aname = fname;
23
+ this.imap.set(fname, img); // record image as loaded!
24
+ res(img);
25
+ });
26
+ img.onerror = ((err) => rej(`failed to load ${url} -> ${err}`));
27
+ img.src = url; // start loading
28
+ });
29
+ // ip['Aname'] = `${fname}-${++ImageLoader.ipser}`;
30
+ this.ipmap.set(fname, ip);
31
+ return ip;
32
+ }
33
+ /**
34
+ * load all fnames, return Promise.all()
35
+ * @param fnames
36
+ */
37
+ loadImages(fnames = this.fnames, ext = this.ext) {
38
+ fnames.forEach(fname => this.ipmap.set(fname, this.loadImage(fname, ext)));
39
+ return this.imageMapPromise = Promise.all(this.ipmap.values()).then((images) => this.imap, (reason) => {
40
+ console.error(stime(this, `loadImages failed: ${reason}`));
41
+ return this.imap;
42
+ });
43
+ }
44
+ /**
45
+ *
46
+ * @param args -
47
+ * - root: path to image directory with trailing '/'
48
+ * - fnames: string[] basenames of each image to load
49
+ * - ext: file extension (for ex: 'png' or 'jpg')
50
+ *
51
+ * @param imap supply or create new Map()
52
+ * @param cb invoked with (imap)
53
+ */
54
+ constructor(args, cb) {
55
+ this.root = args.root;
56
+ this.fnames = args.fnames;
57
+ this.ext = args.ext;
58
+ if (cb) {
59
+ this.loadImages().then(imap => cb(imap));
60
+ }
61
+ }
62
+ imap = new Map();
63
+ ipmap = new Map();
64
+ root;
65
+ fnames;
66
+ ext;
67
+ imagePromises;
68
+ imageMapPromise;
69
+ }
package/dist/meeple.js ADDED
@@ -0,0 +1,152 @@
1
+ import { Shape } from "@thegraid/easeljs-module";
2
+ import { C1, PaintableShape } from "./shapes";
3
+ import { TP } from "./table-params";
4
+ import { Tile } from "./tile";
5
+ class MeepleShape extends PaintableShape {
6
+ player;
7
+ radius;
8
+ static fillColor = 'rgba(225,225,225,.7)';
9
+ static backColor = 'rgba(210,210,120,.5)'; // transparent light green
10
+ constructor(player, radius = TP.meepleRad) {
11
+ super((color) => this.mscgf(color));
12
+ this.player = player;
13
+ this.radius = radius;
14
+ this.y = TP.meepleY0;
15
+ this.setMeepleBounds();
16
+ this.backSide = this.makeOverlay(this.y);
17
+ }
18
+ setMeepleBounds(r = this.radius) {
19
+ this.setBounds(-r, -r, 2 * r, 2 * r);
20
+ }
21
+ backSide; // visible when Meeple is 'faceDown' after a move.
22
+ makeOverlay(y0) {
23
+ const { x, width: w } = this.getBounds();
24
+ const over = new Shape();
25
+ over.graphics.f(MeepleShape.backColor).dc(x + w / 2, y0, w / 2);
26
+ over.visible = false;
27
+ over.name = over.Aname = 'backSide';
28
+ return over;
29
+ }
30
+ /** stroke a ring of colorn, stroke-width = 2, r = radius-2; fill disk with (~WHITE,.7) */
31
+ mscgf(colorn = this.player?.colorn ?? C1.grey, ss = 2, rs = 0) {
32
+ const r = this.radius;
33
+ const g = this.graphics.c().ss(ss).s(colorn).f(MeepleShape.fillColor).dc(0, 0, r - rs - ss / 2); // disk & ring
34
+ return g;
35
+ }
36
+ }
37
+ export class Meeple extends Tile {
38
+ static allMeeples = [];
39
+ get backSide() { return this.baseShape.backSide; }
40
+ get recycleVerb() { return 'dismissed'; }
41
+ /**
42
+ * Meeple - Leader, Police, Criminal
43
+ * @param Aname
44
+ * @param player (undefined for Chooser)
45
+ * @param civicTile Tile where this Meeple spawns
46
+ */
47
+ constructor(Aname, player) {
48
+ super(Aname, player);
49
+ this.addChild(this.backSide);
50
+ this.player = player;
51
+ this.nameText.visible = true;
52
+ this.nameText.y = this.baseShape.y;
53
+ // this.paint();
54
+ Meeple.allMeeples.push(this);
55
+ }
56
+ /** the map Hex on which this Meeple sits. */
57
+ get hex() { return this._hex; }
58
+ /** only one Meep on a Hex, Meep on only one Hex */
59
+ set hex(hex) {
60
+ if (this.hex?.meep === this)
61
+ this.hex.meep = undefined;
62
+ this._hex = hex;
63
+ if (hex !== undefined)
64
+ hex.meep = this;
65
+ }
66
+ get radius() { return TP.meepleRad; } // 31.578 vs 60*.4 = 24
67
+ textVis(v) { super.textVis(true); }
68
+ makeShape() { return new MeepleShape(this.player, this.radius); }
69
+ /** location at start-of-turn; for Meeples.unMove() */
70
+ startHex;
71
+ // we need to unMove meeples in the proper order; lest we get 2 meeps on a hex.
72
+ // meepA -> hexC, meepB -> hexA; undo: meepA -> hexA (collides with meepB), meepB -> hexB
73
+ // Assert: if meepA.startHex is occupied by meepB, then meepB is NOT on meepB.startHex;
74
+ // So: recurse to move meepB to its startHex;
75
+ // Note: with multiple/illegal moves, meepA -> hexB, meepB -> hexA; infinite recurse
76
+ // So: remove meepA from hexB before moving meepB -> hexB
77
+ unMove() {
78
+ if (this.hex === this.startHex)
79
+ return;
80
+ this.placeTile(undefined, false); // take meepA off the map;
81
+ this.startHex.meep?.unMove(); // recurse to move meepB to meepB.startHex
82
+ this.placeTile(this.startHex, false); // Move & update influence; Note: no unMove for Hire! (sendHome)
83
+ this.faceUp();
84
+ }
85
+ /** start of turn, faceUp(undefined) --> faceUp; moveTo(true|false) --> faceUp|faceDn */
86
+ faceUp(up = true) {
87
+ if (this.backSide)
88
+ this.backSide.visible = !up;
89
+ if (up && this.hex)
90
+ this.startHex = this.hex; // set at start of turn.
91
+ this.updateCache();
92
+ if (this.hex?.isOnMap)
93
+ this.gamePlay.hexMap.update();
94
+ }
95
+ moveTo(hex) {
96
+ const destMeep = hex?.meep;
97
+ if (destMeep && destMeep !== this) {
98
+ destMeep.x += 10; // make double occupancy apparent [until this.unMove()]
99
+ destMeep.unMove();
100
+ }
101
+ const fromHex = this.fromHex;
102
+ super.moveTo(hex); // hex.set(meep) = this; this.x/y = hex.x/y
103
+ this.faceUp(!(hex?.isOnMap && fromHex?.isOnMap && hex !== this.startHex));
104
+ }
105
+ cantBeMovedBy(player, ctx) {
106
+ const reason1 = super.cantBeMovedBy(player, ctx);
107
+ if (reason1 || reason1 === false)
108
+ return reason1;
109
+ // if (!ctx?.lastShift && !this.canAutoUnmove && this.backSide.visible) return "already moved"; // no move if not faceUp
110
+ return undefined;
111
+ }
112
+ isOnLine(hex0, fromHex = this.hex) {
113
+ return !!fromHex.linkDirs.find(dir => fromHex.hexesInDir(dir).includes(hex0));
114
+ // return !!fromHex.linkDirs.find(dir => fromHex.findInDir(dir, hex => hex === hex0));
115
+ // return !!fromHex.findLinkHex((hex, dir) => !!hex.findInDir(dir, hex => hex === hex0));
116
+ }
117
+ get canAutoUnmove() { return this.player?.allOnMap(Meeple).filter(meep => meep.hex !== meep.startHex).length == 1; }
118
+ /** override markLegal(), if *this* is the only meeple to have moved,
119
+ * unMove it to reset influence; can always move back to startHex; */
120
+ markLegal(table, setLegal, ctx) {
121
+ if (!ctx?.lastShift && !!setLegal && this.canAutoUnmove) {
122
+ this.unMove(); // this.hex = this.startHex;
123
+ }
124
+ super.markLegal(table, setLegal);
125
+ if (this.fromHex)
126
+ this.fromHex.isLegal = !!setLegal; // if (this.startHex) [not all Tiles have a fromHex!]
127
+ return;
128
+ }
129
+ isLegalTarget0(hex, ctx) {
130
+ if (!hex)
131
+ return false;
132
+ if (hex.meep)
133
+ return false;
134
+ if (!hex.isOnMap)
135
+ return false; // RecycleHex is "on" the map?
136
+ if (!ctx?.lastShift && this.backSide.visible)
137
+ return false;
138
+ return true;
139
+ }
140
+ isLegalTarget(hex, ctx) {
141
+ return this.isLegalTarget0(hex, ctx);
142
+ }
143
+ isLegalRecycle(ctx) {
144
+ if (this.player === this.gamePlay.curPlayer)
145
+ return true;
146
+ return false;
147
+ }
148
+ resetTile() {
149
+ this.faceUp();
150
+ this.startHex = undefined;
151
+ }
152
+ }
@@ -0,0 +1,33 @@
1
+ import { stime } from "@thegraid/common-lib";
2
+ class mockPlanner {
3
+ waitPaused(ident) {
4
+ throw new Error("Method not implemented.");
5
+ }
6
+ pause() {
7
+ throw new Error("Method not implemented.");
8
+ }
9
+ resume() {
10
+ throw new Error("Method not implemented.");
11
+ }
12
+ roboMove(run) {
13
+ throw new Error("Method not implemented.");
14
+ }
15
+ terminate() {
16
+ console.log(stime(this, `.terminate: TBD`));
17
+ }
18
+ }
19
+ export class Planner extends mockPlanner {
20
+ pauseP;
21
+ }
22
+ /**
23
+ * IPlanner factory method, invoked from Player.newGame()
24
+ * @param hexMap from the main GamePlay, location of Hex for makeMove
25
+ * @param index player.index [0 -> 'b', 1 -> 'w']
26
+ * @returns Planner or PlannerProxy
27
+ */
28
+ export function newPlanner(hexMap, index) {
29
+ // let planner = TP.pWorker
30
+ // ? new PlannerProxy(hexMap.mh, hexMap.nh, index, logWriter) // -> Remote Planner [no Parallel]
31
+ // : new Planner(hexMap.mh, hexMap.nh, index, logWriter) // -> Local ParallelPlanner *or* Planner
32
+ return new mockPlanner();
33
+ }
@@ -0,0 +1,125 @@
1
+ import { CenterText, S, stime } from "@thegraid/easeljs-lib";
2
+ import { Container, Graphics, MouseEvent } from "@thegraid/easeljs-module";
3
+ import { NamedContainer } from "./game-play";
4
+ import { RectShape, UtilButton } from "./shapes";
5
+ import { TP } from "./table-params";
6
+ export class PlayerPanel extends NamedContainer {
7
+ table;
8
+ player;
9
+ high;
10
+ wide;
11
+ dir;
12
+ outline;
13
+ get hexMap() { return this.table.gamePlay.hexMap; }
14
+ /**
15
+ *
16
+ * @param table
17
+ * @param player
18
+ * @param high
19
+ * @param wide
20
+ * @param row
21
+ * @param col
22
+ * @param dir
23
+ */
24
+ constructor(table, player, high, wide, row, col, dir = -1) {
25
+ super(player.Aname); // for debugger
26
+ this.table = table;
27
+ this.player = player;
28
+ this.high = high;
29
+ this.wide = wide;
30
+ this.dir = dir;
31
+ table.hexMap.mapCont.resaCont.addChild(this);
32
+ table.setToRowCol(this, row, col);
33
+ this.setOutline();
34
+ this.makeConfirmation();
35
+ }
36
+ get metrics() {
37
+ const { dxdc, dydr } = this.table.hexMap.xywh, dir = this.dir;
38
+ const wide = dxdc * this.wide, high = dydr * this.high, brad = TP.hexRad, gap = 6, rowh = 2 * brad + gap;
39
+ return { dir, dydr, wide, high, brad, gap, rowh };
40
+ }
41
+ get objects() {
42
+ const player = this.player, index = player.index, panel = this;
43
+ const table = this.table, gamePlay = this.player.gamePlay;
44
+ return { panel, player, index, table, gamePlay };
45
+ }
46
+ /**
47
+ *
48
+ * @param t1 stroke width (2)
49
+ * @param bgc fill color
50
+ */
51
+ setOutline(t1 = 2, bgc = this.bg0) {
52
+ const { wide, high, brad, gap } = this.metrics;
53
+ const t2 = t1 * 2 + 1, g = new Graphics().ss(t2);
54
+ this.removeChild(this.outline);
55
+ this.outline = new RectShape({ x: -t1, y: -t1, w: wide + t2, h: high + t2 }, bgc, this.player.color, g);
56
+ this.addChildAt(this.outline, 0);
57
+ }
58
+ bg0 = 'rgba(255,255,255,.3)';
59
+ bg1 = 'rgba(255,255,255,.5)';
60
+ showPlayer(show = (this.player && this.player === this.player.gamePlay.curPlayer)) {
61
+ this.setOutline(show ? 4 : 2, show ? this.bg1 : this.bg0);
62
+ }
63
+ confirmContainer;
64
+ makeConfirmation() {
65
+ const { wide, high, brad, gap, rowh } = this.metrics;
66
+ const { table } = this.objects;
67
+ const conf = this.confirmContainer = new Container();
68
+ conf.name = 'confirm';
69
+ const bg0 = new RectShape({ x: 0, y: -brad - gap, w: wide, h: high }, '', '');
70
+ bg0.paint('rgba(240,240,240,.2)');
71
+ const bg1 = new RectShape({ x: 0, y: 4 * rowh - brad - 2 * gap, w: wide, h: high - 4 * rowh + gap }, '', '');
72
+ bg1.paint('rgba(240,240,240,.8)');
73
+ const title = conf.titleText = new CenterText('Are you sure?', 30);
74
+ title.x = wide / 2;
75
+ title.y = 3.85 * rowh;
76
+ const msgText = conf.messageText = new CenterText('', 30);
77
+ msgText.x = wide / 2;
78
+ msgText.y = 5 * rowh;
79
+ const button1 = conf.buttonYes = new UtilButton('lightgreen', 'Yes', TP.hexRad);
80
+ const button2 = conf.buttonCan = new UtilButton('rgb(255, 100, 100)', 'Cancel', TP.hexRad);
81
+ button1.y = button2.y = 6 * rowh;
82
+ button1.x = wide * .4;
83
+ button2.x = wide * .6;
84
+ conf.addChild(bg0, bg1, button1, button2, msgText, title);
85
+ conf.visible = false;
86
+ table.overlayCont.addChild(conf);
87
+ }
88
+ /** keybinder access to areYouSure */
89
+ clickConfirm(yes = true) {
90
+ // let target = (this.confirmContainer.children[2] as UtilButton);
91
+ if (!this.confirmContainer.visible)
92
+ return;
93
+ const buttonYes = this.confirmContainer.buttonYes;
94
+ const buttonCan = this.confirmContainer.buttonCan;
95
+ const nativeMouseEvent = undefined;
96
+ const event = new MouseEvent(S.click, false, true, 0, 0, nativeMouseEvent, -1, true, 0, 0);
97
+ (yes ? buttonYes : buttonCan).dispatchEvent(event);
98
+ }
99
+ areYouSure(msg, yes, cancel, afterUpdate = () => { }) {
100
+ const { panel, table } = this.objects, doneVis = table.doneButton.visible;
101
+ table.doneButton.mouseEnabled = table.doneButton.visible = false;
102
+ const conf = this.confirmContainer;
103
+ const button1 = conf.buttonYes;
104
+ const button2 = conf.buttonCan;
105
+ const msgText = conf.children[4];
106
+ msgText.text = msg;
107
+ const clear = (func) => {
108
+ conf.visible = false;
109
+ button1.removeAllEventListeners();
110
+ button2.removeAllEventListeners();
111
+ table.doneButton.mouseEnabled = table.doneButton.visible = doneVis;
112
+ button1.updateWait(false, func);
113
+ };
114
+ button2.visible = !!cancel;
115
+ button1.label_text = !!cancel ? 'Yes' : 'Continue';
116
+ conf.titleText.text = !!cancel ? 'Are your sure?' : 'Click to Confirm';
117
+ button1.on(S.click, () => clear(yes), this, true);
118
+ button2.on(S.click, () => clear(cancel ?? yes), this, true);
119
+ console.log(stime(this, `.areYouSure? [${this.player.Aname}], ${msg}`));
120
+ panel.localToLocal(0, 0, table.overlayCont, conf);
121
+ conf.visible = true;
122
+ button1.updateWait(false, afterUpdate);
123
+ // setTimeout(cancel, 500);
124
+ }
125
+ }
package/dist/player.js ADDED
@@ -0,0 +1,112 @@
1
+ import { stime } from "@thegraid/common-lib";
2
+ import { Meeple } from "./meeple";
3
+ import { TP } from "./table-params";
4
+ import { MapTile, Tile, } from "./tile";
5
+ export class Player {
6
+ index;
7
+ gamePlay;
8
+ static allPlayers = [];
9
+ static colorScheme = ['Red', 'Blue', 'darkgreen', 'Violet', 'gold', 'purple'];
10
+ Aname;
11
+ constructor(index, gamePlay) {
12
+ this.index = index;
13
+ this.gamePlay = gamePlay;
14
+ Player.allPlayers[index] = this;
15
+ this.color = Player.colorScheme[index];
16
+ this.Aname = `P${index}:${this.color}`;
17
+ console.log(stime(this, `.new:`), this.Aname);
18
+ }
19
+ _color;
20
+ get color() { return this._color; }
21
+ set color(c) { this._color = c; }
22
+ /** much useful context about this Player. */
23
+ panel;
24
+ get colorn() { return this._color; }
25
+ // Player shall paint their pieces to their color as necessary;
26
+ // Other code shall operate using player.index;
27
+ allOf(claz) { return Tile.allTiles.filter(t => t instanceof claz && t.player === this); }
28
+ allOnMap(claz) { return this.allOf(claz).filter(t => t.hex?.isOnMap); }
29
+ /** Resi/Busi/PS/Lake/Civics in play on Map */
30
+ get mapTiles() { return this.allOf(MapTile); }
31
+ // Player's Leaders, Police & Criminals
32
+ get meeples() { return Meeple.allMeeples.filter(meep => meep.player == this); }
33
+ ;
34
+ _score = 0;
35
+ get score() { return this._score; }
36
+ set score(score) {
37
+ this._score = Math.floor(score);
38
+ }
39
+ // Created in masse by Table.layoutCounter
40
+ coinCounter; // set by layoutCounters: `${'Coin'}Counter`
41
+ get coins() { return this.coinCounter?.getValue(); }
42
+ set coins(v) { this.coinCounter?.updateValue(v); }
43
+ get otherPlayer() { return Player.allPlayers[1 - this.index]; }
44
+ planner;
45
+ /** if true then invoke plannerMove */
46
+ useRobo = false;
47
+ startDir;
48
+ /** make Civics, Leaders & Police; also makeLeaderHex() */
49
+ makePlayerBits() {
50
+ }
51
+ get isDestroyed() {
52
+ return this.allOnMap(MapTile).length == 0;
53
+ }
54
+ get isComplete() {
55
+ return false;
56
+ }
57
+ endGame() {
58
+ this.planner?.terminate();
59
+ this.planner = undefined;
60
+ }
61
+ static remotePlayer = 1; // temporary, bringup-debug: index of 'remotePlayer' (see below)
62
+ /**
63
+ * Before start each new game.
64
+ *
65
+ * [make newPlanner for this Player]
66
+ */
67
+ newGame(gamePlay, url = TP.networkUrl) {
68
+ this.planner?.terminate();
69
+ // this.hgClient = (this.index == Player.remotePlayer) ? new HgClient(url, (hgClient) => {
70
+ // console.log(stime(this, `.hgClientOpen!`), hgClient)
71
+ // }) : undefined
72
+ // this.planner = newPlanner(gamePlay.hexMap, this.index)
73
+ }
74
+ newTurn() {
75
+ // faceUp and record start location:
76
+ this.meeples.forEach(meep => meep.faceUp()); // set meep.startHex for unMove
77
+ }
78
+ stopMove() {
79
+ this.planner?.roboMove(false);
80
+ }
81
+ /** if Planner is not running, maybe start it; else wait for GUI */ // TODO: move Table.dragger to HumanPlanner
82
+ playerMove(useRobo = this.useRobo, incb = 0) {
83
+ let running = this.plannerRunning;
84
+ // feedback for KeyMove:
85
+ TP.log > 0 && console.log(stime(this, `(${this.colorn}).playerMove(${useRobo}): useRobo=${this.useRobo}, running=${running}`));
86
+ if (running)
87
+ return;
88
+ if (useRobo || this.useRobo) {
89
+ // start plannerMove from top of stack:
90
+ // setTimeout(() => this.plannerMove(incb))
91
+ }
92
+ return; // robo or GUI will invoke gamePlay.doPlayerMove(...)
93
+ }
94
+ plannerRunning = false;
95
+ plannerMove(incb = 0) {
96
+ this.planner?.roboMove(true);
97
+ this.plannerRunning = true;
98
+ // let iHistory = this.table.gamePlay.iHistory
99
+ // let ihexPromise = this.planner.makeMove(sc, iHistory, incb)
100
+ // ihexPromise.then((ihex: IHex) => {
101
+ // this.plannerRunning = false
102
+ // this.table.moveStoneToHex(ihex, sc)
103
+ // })
104
+ }
105
+ }
106
+ class RemotePlayer extends Player {
107
+ newGame(gamePlay) {
108
+ this.planner?.terminate();
109
+ // this.hgClient = (this.index == RemotePlayer.remotePlayer) ? new HgClient() : undefined
110
+ // this.planner = newPlanner(gamePlay.hexMap, this.index, gamePlay.logWriter)
111
+ }
112
+ }
@@ -0,0 +1,67 @@
1
+ // TODO: namespace or object for GameState names
2
+ import { S, stime } from "@thegraid/common-lib";
3
+ import { KeyBinder } from "@thegraid/easeljs-lib";
4
+ export class ScenarioParser {
5
+ map;
6
+ gamePlay;
7
+ constructor(map, gamePlay) {
8
+ this.map = map;
9
+ this.gamePlay = gamePlay;
10
+ }
11
+ // coins, score, actions, events, AnkhPowers, Guardians in stable; specials for Amun, Bastet, Horus, ...
12
+ parseScenario(setup) {
13
+ if (!setup)
14
+ return;
15
+ // console.log(stime(this, `.parseScenario: curState =`), this.saveState(this.gamePlay, true)); // log current state for debug...
16
+ console.log(stime(this, `.parseScenario: newState =`), setup);
17
+ const { gameState, turn } = setup;
18
+ const map = this.map, gamePlay = this.gamePlay, allPlayers = gamePlay.allPlayers, table = gamePlay.table;
19
+ const turnSet = (turn !== undefined); // indicates a Saved Scenario: assign & place everything
20
+ if (turnSet) {
21
+ gamePlay.turnNumber = turn;
22
+ table.logText(`turn = ${turn}`, `parseScenario`);
23
+ this.gamePlay.allTiles.forEach(tile => tile.hex?.isOnMap ? tile.sendHome() : undefined); // clear existing map
24
+ }
25
+ if (gameState) {
26
+ this.gamePlay.gameState.parseState(gameState);
27
+ }
28
+ this.gamePlay.hexMap.update();
29
+ }
30
+ saveState(gamePlay, silent = false) {
31
+ const turn = Math.max(0, gamePlay.turnNumber);
32
+ const coins = gamePlay.allPlayers.map(p => p.coins);
33
+ const time = stime.fs();
34
+ const gameState = this.gamePlay.gameState.saveState();
35
+ const setupElt = { turn, time, coins, gameState, };
36
+ this.logState(setupElt);
37
+ return setupElt;
38
+ }
39
+ /** write each component of SetupElt on a line, wrapped between '{' ... '\n}' */
40
+ logState(state, logWriter = this.gamePlay.logWriter) {
41
+ let lines = '{', keys = Object.keys(state), n = keys.length - 1;
42
+ keys.forEach((key, ndx) => {
43
+ const line = JSON.stringify(state[key]);
44
+ lines = `${lines}\n ${key}: ${line}${ndx < n ? ',' : ''}`;
45
+ });
46
+ lines = `${lines}\n},`;
47
+ logWriter.writeLine(lines);
48
+ }
49
+ /** debug utility */
50
+ identCells(map) {
51
+ map.forEachHex(hex => {
52
+ const hc = hex.cont;
53
+ hc.mouseEnabled = true;
54
+ hc.on(S.click, () => {
55
+ hex.isLegal = !hex.isLegal;
56
+ map.update();
57
+ });
58
+ });
59
+ KeyBinder.keyBinder.setKey('x', {
60
+ func: () => {
61
+ const cells = map.filterEachHex(hex => hex.isLegal);
62
+ const list = cells.map(hex => `${hex.rcs},`);
63
+ console.log(''.concat(...list));
64
+ }
65
+ });
66
+ }
67
+ }