@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,59 @@
1
+ import { C, S } from "@thegraid/common-lib";
2
+ import { BoolChoice, Chooser, DropdownButton, DropdownChoice, EditBox, KeyBinder } from "@thegraid/easeljs-lib";
3
+ /** no choice: a DropdownChoice with 1 mutable item that can be set by setValue(...) */
4
+ export class NC extends DropdownChoice {
5
+ static style(defStyle) {
6
+ let baseStyle = DropdownButton.mergeStyle(defStyle);
7
+ let pidStyle = { arrowColor: 'transparent', textAlign: 'right' };
8
+ return DropdownButton.mergeStyle(pidStyle, baseStyle);
9
+ }
10
+ constructor(items, item_w, item_h, defStyle = {}) {
11
+ super(items, item_w, item_h, NC.style(defStyle));
12
+ }
13
+ /** never expand */
14
+ rootclick() { }
15
+ setValue(value, item, target) {
16
+ item.value = value; // for reference?
17
+ this._rootButton.text.text = value;
18
+ return true;
19
+ }
20
+ }
21
+ /** Chooser with an EditBox
22
+ *
23
+ * Bind M-v to pasteClipboard()
24
+ */
25
+ export class EBC extends Chooser {
26
+ editBox;
27
+ constructor(items, item_w, item_h, style = {}) {
28
+ super(items, item_w, item_h, style);
29
+ style && (style.bgColor = style.fillColor);
30
+ style && (style.textColor = C.BLACK);
31
+ this.editBox = new EditBox({ x: 0, y: 0, w: item_w, h: item_h * 1 }, style);
32
+ this.addChild(this.editBox);
33
+ const scope = this.editBox.keyScope;
34
+ KeyBinder.keyBinder.setKey('M-v', () => this.pasteClipboard, scope);
35
+ this.on(S.click, () => this.editBox.setFocus(true), this);
36
+ }
37
+ /**
38
+ * insert text from window system clipboard: await navigator.clipboard.readText();
39
+ * @param pt where to inject text [this.point]
40
+ * @param n number of following chars to delete [0], if n<0 delete ALL following chars
41
+ */
42
+ pasteClipboard(pt = this.editBox.point, n = 0) {
43
+ const paste = async () => {
44
+ let text = await navigator.clipboard.readText();
45
+ this.editBox.splice(pt, n < 0 ? undefined : n, text);
46
+ };
47
+ paste();
48
+ }
49
+ setValue(value, item, target) {
50
+ this.editBox.setText(value);
51
+ return true;
52
+ }
53
+ }
54
+ /** like StatsPanel: read-only output field */
55
+ export class PidChoice extends NC {
56
+ }
57
+ /** present [false, true] with any pair of string: ['false', 'true'] */
58
+ export class BC extends BoolChoice {
59
+ }
@@ -0,0 +1,155 @@
1
+ import { S } from "@thegraid/common-lib";
2
+ import { ValueCounter, ValueEvent } from "@thegraid/easeljs-lib"; // "./value-counter";
3
+ import { Shape } from "@thegraid/easeljs-module";
4
+ /** ValueCounter in a Rectangle. */
5
+ export class ValueCounterBox extends ValueCounter {
6
+ /** return width, height; suitable for makeBox() => drawRect() */
7
+ boxSize(text) {
8
+ const width = text.getMeasuredWidth();
9
+ const height = text.getMeasuredLineHeight();
10
+ const high = height * 1.1; // change from ellispe margins
11
+ const wide = Math.max(width * 1.1, high); // change from ellispe margins
12
+ return { width: wide, height: high };
13
+ }
14
+ makeBox(color, high, wide) {
15
+ let shape = new Shape();
16
+ shape.graphics.c().f(color).drawRect(-wide / 2, -high / 2, wide, high); // change from ellispe
17
+ return shape;
18
+ }
19
+ }
20
+ export class ButtonBox extends ValueCounterBox {
21
+ constructor(name, initValue, color, fontSize, fontName, textColor) {
22
+ super(name, initValue, color, fontSize, fontName, textColor);
23
+ this.mouseEnabled = true;
24
+ }
25
+ }
26
+ /** ValueCounter specifically for number values (not string), includes incValueEvent() and clickToInc() */
27
+ export class NumCounter extends ValueCounter {
28
+ setValue(value) {
29
+ super.setValue(value);
30
+ }
31
+ getValue() {
32
+ return super.getValue() ?? 0;
33
+ }
34
+ incValue(incr) {
35
+ this.updateValue(this.getValue() + incr);
36
+ this.dispatchEvent(new ValueEvent('incr', incr));
37
+ }
38
+ /**
39
+ *
40
+ * @param incr configure click/incValue:
41
+ * - false: click does nothing
42
+ * - !false: click -> this.incValue()
43
+ * - NumCounter: this.incValue(x) -> incr.incValue(x)
44
+ */
45
+ clickToInc(incr = true) {
46
+ const incv = (evt) => (evt?.ctrlKey ? -1 : 1) * (evt?.shiftKey ? 10 : 1);
47
+ if (incr) {
48
+ this.mouseEnabled = true;
49
+ this.on(S.click, (evt) => this.incValue(incv(evt.nativeEvent)));
50
+ if (incr instanceof NumCounter) {
51
+ this.on('incr', (evt) => incr.incValue(evt.value));
52
+ }
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * NumCounterBoxLabeled: larger box to include the label.
58
+ */
59
+ export class NumCounterBox extends NumCounter {
60
+ labelH = 0;
61
+ setLabel(label, offset, fontSize) {
62
+ fontSize = fontSize ?? this.labelFontSize;
63
+ offset = offset ?? { x: this.label?.x ?? 0, y: this.label?.y || (fontSize / 2) };
64
+ super.setLabel(label, offset, fontSize);
65
+ this.labelH = this.label?.text ? this.labelFontSize ?? 0 : 0;
66
+ this.wide = -1; // force new box
67
+ this.setBoxWithValue(this.value);
68
+ }
69
+ makeBox0(color, high, wide) {
70
+ const shape = new Shape();
71
+ shape.graphics.c().f(color).drawRect(-wide / 2, -high / 2, wide, high); // centered on {x,y}
72
+ return shape;
73
+ }
74
+ makeBox(color, high, wide) {
75
+ const yinc = this.label ? this.labelFontSize / 2 : 0; // dubious math; but works for now...
76
+ const shape = this.makeBox0(color, high + yinc, wide); // 4 px beneath for label
77
+ shape.y += yinc / 2;
78
+ return shape;
79
+ }
80
+ /** return width, height; suitable for makeBox() => drawRect() */
81
+ boxSize(text) {
82
+ const width = text.getMeasuredWidth();
83
+ const height = text.getMeasuredLineHeight();
84
+ const high = height * 1.1; // change from ellispe margins
85
+ const wide = Math.max(width * 1.1, high); // change from ellispe margins
86
+ return { width: wide, height: high };
87
+ }
88
+ }
89
+ export class NoZeroCounter extends NumCounter {
90
+ setBoxWithValue(value) {
91
+ super.setBoxWithValue(value || '');
92
+ }
93
+ }
94
+ export class DecimalCounter extends NumCounterBox {
95
+ decimal = 0;
96
+ constructor(name, initValue, color, fontSize, fontName) {
97
+ super(name, initValue, color, fontSize, fontName);
98
+ }
99
+ setBoxWithValue(value) {
100
+ super.setBoxWithValue(value.toFixed(this.decimal));
101
+ }
102
+ }
103
+ export class PerRoundCounter extends DecimalCounter {
104
+ gamePlay;
105
+ get perRound() { return this.value / Math.max(1, Math.floor(this.gamePlay.turnNumber / 2)); }
106
+ decimal = 1;
107
+ setBoxWithValue(value) {
108
+ super.setBoxWithValue(this.perRound);
109
+ }
110
+ }
111
+ // export class CostIncCounter extends NumCounter {
112
+ // /**
113
+ // * Show InfR for curPlayer to place Tile;
114
+ // * @param hex place Counter above the given hex.
115
+ // * @param name internal identifyier
116
+ // * @param ndx cost increment based on CostIncMatrix[ndx]; -1 -> show no cost
117
+ // * @param repaint calc cost for: Player OR true/false->curPlayer;
118
+ // * - Note: false -> const cost, no repaint
119
+ // */
120
+ // constructor(
121
+ // public hex: Hex2,
122
+ // name = `costInc`,
123
+ // public ndx = -1,
124
+ // public repaint: boolean | Player = true
125
+ // ) {
126
+ // super(name, 0, 'grey', TP.hexRad / 2)
127
+ // const counterCont = hex.mapCont.counterCont;
128
+ // const xy = hex.cont.localToLocal(0, TP.hexRad * H.sqrt3_2, counterCont);
129
+ // this.attachToContainer(counterCont, xy);
130
+ // }
131
+ // protected override makeBox(color: string, high: number, wide: number): DisplayObject {
132
+ // const box = new InfShape('lightgrey');
133
+ // const size = Math.max(high, wide)
134
+ // box.scaleX = box.scaleY = .5 * size / TP.hexRad;
135
+ // return box
136
+ // }
137
+ // /** return width, height; suitable for makeBox() => drawRect() */
138
+ // protected override boxSize(text: Text): { width: number; height: number } {
139
+ // let width = text.getMeasuredWidth();
140
+ // let height = text.getMeasuredLineHeight();
141
+ // let high = height * 1.1;
142
+ // let wide = Math.max(width * 1.1, high);
143
+ // let rv = { width: wide, height: high };
144
+ // return rv;
145
+ // }
146
+ // }
147
+ // class CostTotalCounter extends CostIncCounter {
148
+ // protected override makeBox(color: string, high: number, wide: number): DisplayObject {
149
+ // let box = new Shape();
150
+ // let size = Math.max(high, wide)
151
+ // box.graphics.c().f(C.coinGold).dc(0, 0, TP.hexRad);
152
+ // box.scaleX = box.scaleY = .5 * size / TP.hexRad;
153
+ // return box
154
+ // }
155
+ // }
@@ -0,0 +1,455 @@
1
+ import { json } from "@thegraid/common-lib";
2
+ import { KeyBinder, S, Undo, blinkAndThen, stime } from "@thegraid/easeljs-lib";
3
+ import { Container } from "@thegraid/easeljs-module";
4
+ import { GameState } from "./game-state";
5
+ import { Hex, Hex2, HexMap } from "./hex";
6
+ import { Meeple } from "./meeple";
7
+ import { Player } from "./player";
8
+ import { TP } from "./table-params";
9
+ import { Tile } from "./tile";
10
+ export class NamedContainer extends Container {
11
+ Aname;
12
+ constructor(name, cx = 0, cy = 0) {
13
+ super();
14
+ this.Aname = this.name = name;
15
+ this.x = cx;
16
+ this.y = cy;
17
+ }
18
+ }
19
+ class HexEvent {
20
+ }
21
+ class Move {
22
+ Aname = "";
23
+ ind = 0;
24
+ board = {};
25
+ }
26
+ /** Implement game, enforce the rules, manage GameStats & hexMap; no GUI/Table required.
27
+ *
28
+ * Actions are:
29
+ * - Reserve: place one Tile from auction to Player reserve
30
+ * - Recruit: place a Builder/Leader (in Civic);
31
+ * do Build/Police action (requires 5 Econ)
32
+ * - Build: move Master/Builders, build one Tile (from auction or reserve)
33
+ * - Police: place one (in Station), move police (& leaders/builders), attack/capture;
34
+ * collatoral damge (3 Econ); dismiss Police
35
+ * - Crime: place one on unoccupied hex adjacent to opponent Tile (requires 3 Econ)
36
+ * move Criminals, attack/capture;
37
+ * (Player keeps the captured Tile/Meeple; maybe earn VP if Crime Lord)
38
+ * -
39
+ */
40
+ export class GamePlay0 {
41
+ gameSetup;
42
+ /** the latest GamePlay instance in this VM/context/process */
43
+ static gamePlay;
44
+ static gpid = 0;
45
+ id = GamePlay0.gpid++;
46
+ gameState = (this instanceof GamePlay) ? new GameState(this) : undefined;
47
+ get gamePhase() { return this.gameState.state; }
48
+ isPhase(name) { return this.gamePhase === this.gameState.states[name]; }
49
+ phaseDone(...args) { this.gameState.done(...args); }
50
+ recycleHex;
51
+ ll(n) { return TP.log > n; }
52
+ get logWriter() { return this.gameSetup.logWriter; }
53
+ get allPlayers() { return Player.allPlayers; }
54
+ get allTiles() { return Tile.allTiles; }
55
+ hexMap = new HexMap(TP.hexRad, true, Hex2); // create base map; no districts until Table.layoutTable!
56
+ history = []; // sequence of Move that bring board to its state
57
+ redoMoves = [];
58
+ logWriterLine0() {
59
+ const setup = this.gameSetup, thus = this, turn = thus.turnNumber;
60
+ let line = { time: stime.fs(), turn };
61
+ let line0 = json(line, true); // machine readable starting conditions
62
+ console.log(`-------------------- ${line0}`);
63
+ this.logWriter.writeLine(`{start: ${line0}},`);
64
+ }
65
+ /** GamePlay0 - supply GodNames for each: new Player(...). */
66
+ constructor(gameSetup) {
67
+ this.gameSetup = gameSetup;
68
+ this.hexMap.Aname = `mainMap`;
69
+ //this.hexMap.makeAllDistricts(); // For 'headless'; re-created by Table, after addToMapCont()
70
+ }
71
+ turnNumber = 0; // = history.lenth + 1 [by this.setNextPlayer]
72
+ curPlayerNdx = 0; // curPlayer defined in GamePlay extends GamePlay0
73
+ curPlayer;
74
+ preGame = true;
75
+ nextPlayer(plyr = this.curPlayer) {
76
+ const nxt = (plyr.index + 1) % Player.allPlayers.length;
77
+ return Player.allPlayers[nxt];
78
+ }
79
+ forEachPlayer(f) {
80
+ this.allPlayers.forEach((p, index, players) => f(p, index, players));
81
+ }
82
+ logText(line, from = '') {
83
+ if (this instanceof GamePlay)
84
+ this.table.logText(line, from);
85
+ }
86
+ /**
87
+ * When player has completed Actions and Event, do next player.
88
+ */
89
+ endTurn() {
90
+ // Jubilee if win condition:
91
+ if (this.isEndOfGame()) {
92
+ this.endGame();
93
+ }
94
+ else {
95
+ this.setNextPlayer();
96
+ }
97
+ }
98
+ endGame() {
99
+ const scores = [];
100
+ let topScore = -1, winner;
101
+ console.log(stime(this, `.endGame: Game Over`));
102
+ // console.log(stime(this, `.endGame: Winner = ${winner.Aname}`), scores);
103
+ }
104
+ newTurn() { }
105
+ setNextPlayer(turnNumber) {
106
+ if (turnNumber === undefined) {
107
+ this.turnNumber = turnNumber = this.turnNumber + 1;
108
+ this.newTurn(); // override calls saveState()
109
+ }
110
+ this.turnNumber = turnNumber;
111
+ const index = (turnNumber % this.allPlayers.length);
112
+ this.preGame = false;
113
+ this.curPlayerNdx = index;
114
+ this.curPlayer = this.allPlayers[index];
115
+ this.curPlayer.newTurn();
116
+ }
117
+ isEndOfGame() {
118
+ // can only win at the end of curPlayer's turn:
119
+ const endp = false;
120
+ if (endp)
121
+ console.log(stime(this, `.isEndOfGame:`));
122
+ return endp;
123
+ }
124
+ /** Planner may override with alternative impl. */
125
+ newMoveFunc;
126
+ newMove(hex, sc, caps, gp) {
127
+ return this.newMoveFunc ? this.newMoveFunc(hex, sc, caps, gp) : new Move();
128
+ }
129
+ undoRecs = new Undo().enableUndo();
130
+ addUndoRec(obj, name, value = obj.name) {
131
+ this.undoRecs.addUndoRec(obj, name, value);
132
+ }
133
+ /** update Counters (econ, expense, vp) for ALL players. */
134
+ updateCounters() {
135
+ // Player.allPlayers.forEach(player => player.setCounters(false));
136
+ this.hexMap.update();
137
+ }
138
+ logFailure(type, reqd, avail, toHex) {
139
+ const failText = `${type} required: ${reqd} > ${avail}`;
140
+ console.log(stime(this, `.failToPayCost:`), failText, toHex.Aname);
141
+ this.logText(failText, `GamePlay.failToPayCost`);
142
+ }
143
+ /**
144
+ * Move tile to hex (or recycle), updating influence.
145
+ *
146
+ * Tile.dropFunc() -> Tile.placeTile() -> gp.placeEither()
147
+ * @param tile ignore if undefined
148
+ * @param toHex tile.moveTo(toHex)
149
+ * @param payCost commit and verify payment
150
+ */
151
+ placeEither(tile, toHex, payCost) {
152
+ if (!tile)
153
+ return;
154
+ const fromHex = tile.fromHex;
155
+ if (toHex !== fromHex)
156
+ this.logText(`${tile} -> ${toHex}`, `gamePlay.placeEither`);
157
+ tile.moveTo(toHex); // placeEither(tile, hex) --> moveTo(hex)
158
+ if (toHex === this.recycleHex) {
159
+ this.logText(`Recycle ${tile} from ${fromHex?.Aname || '?'}`, `gamePlay.placeEither`);
160
+ this.recycleTile(tile); // Score capture; log; return to homeHex
161
+ }
162
+ this.updateCounters();
163
+ }
164
+ recycleTile(tile) {
165
+ if (!tile)
166
+ return; // no prior reserveTile...
167
+ let verb = tile.recycleVerb ?? 'recycled';
168
+ if (tile.fromHex?.isOnMap) {
169
+ if (tile.player !== this.curPlayer) {
170
+ verb = 'defeated';
171
+ }
172
+ else if (tile instanceof Meeple) {
173
+ }
174
+ }
175
+ tile.logRecycle(verb);
176
+ tile.sendHome(); // recycleTile
177
+ }
178
+ }
179
+ /** GamePlay with Table & GUI (KeyBinder, ParamGUI & Dragger) */
180
+ export class GamePlay extends GamePlay0 {
181
+ table; // access to GUI (drag/drop) methods.
182
+ /** GamePlay is the GUI-augmented extension of GamePlay0; uses Table */
183
+ constructor(scenario, table, gameSetup) {
184
+ super(gameSetup); // hexMap, history, gStats...
185
+ Tile.gamePlay = this; // table
186
+ this.table = table;
187
+ if (this.table.stage.canvas)
188
+ this.bindKeys();
189
+ }
190
+ /** suitable for keybinding */
191
+ unMove() {
192
+ this.curPlayer.meeples.forEach((meep) => meep.hex?.isOnMap && meep.unMove());
193
+ }
194
+ bindKeys() {
195
+ let table = this.table;
196
+ let roboPause = () => { this.forEachPlayer(p => this.pauseGame(p)); };
197
+ let roboResume = () => { this.forEachPlayer(p => this.resumeGame(p)); };
198
+ let roboStep = () => {
199
+ let p = this.curPlayer, op = this.nextPlayer(p);
200
+ this.pauseGame(op);
201
+ this.resumeGame(p);
202
+ };
203
+ // KeyBinder.keyBinder.setKey('p', { thisArg: this, func: roboPause })
204
+ // KeyBinder.keyBinder.setKey('r', { thisArg: this, func: roboResume })
205
+ // KeyBinder.keyBinder.setKey('s', { thisArg: this, func: roboStep })
206
+ // KeyBinder.keyBinder.setKey('R', { thisArg: this, func: () => this.runRedo = true })
207
+ // KeyBinder.keyBinder.setKey('q', { thisArg: this, func: () => this.runRedo = false })
208
+ // KeyBinder.keyBinder.setKey(/1-9/, { thisArg: this, func: (e: string) => { TP.maxBreadth = Number.parseInt(e) } })
209
+ KeyBinder.keyBinder.setKey('M-z', { thisArg: this, func: this.undoMove });
210
+ KeyBinder.keyBinder.setKey('b', { thisArg: this, func: this.undoMove });
211
+ KeyBinder.keyBinder.setKey('f', { thisArg: this, func: this.redoMove });
212
+ //KeyBinder.keyBinder.setKey('S', { thisArg: this, func: this.skipMove })
213
+ KeyBinder.keyBinder.setKey('Escape', { thisArg: table, func: table.stopDragging }); // Escape
214
+ KeyBinder.keyBinder.setKey('C-c', { thisArg: this, func: this.stopPlayer }); // C-c Stop Planner
215
+ KeyBinder.keyBinder.setKey('u', { thisArg: this, func: this.unMove });
216
+ // KeyBinder.keyBinder.setKey('n', () => { this.endTurn(); this.gameState.phase('BeginTurn') });
217
+ KeyBinder.keyBinder.setKey('C-c', { thisArg: this, func: this.reCacheTiles });
218
+ KeyBinder.keyBinder.setKey('c', { thisArg: this, func: this.clickConfirm, argVal: false });
219
+ KeyBinder.keyBinder.setKey('y', { thisArg: this, func: this.clickConfirm, argVal: true });
220
+ KeyBinder.keyBinder.setKey('d', { thisArg: this, func: this.clickDone, argVal: true });
221
+ KeyBinder.keyBinder.setKey('l', () => this.logWriter.pickLogFile());
222
+ KeyBinder.keyBinder.setKey('L', () => this.logWriter.showBacklog());
223
+ KeyBinder.keyBinder.setKey('M-l', () => this.logWriter.closeFile());
224
+ KeyBinder.keyBinder.setKey('C-l', () => this.readFileState());
225
+ KeyBinder.keyBinder.setKey('r', () => this.readFileState());
226
+ KeyBinder.keyBinder.setKey('h', () => { this.table.textLog.visible = !this.table.textLog.visible; this.hexMap.update(); });
227
+ // KeyBinder.keyBinder.setKey('U', { thisArg: this.gameState, func: this.gameState.undoAction, argVal: true })
228
+ KeyBinder.keyBinder.setKey('p', { thisArg: this, func: this.saveState, argVal: true });
229
+ KeyBinder.keyBinder.setKey('P', { thisArg: this, func: this.pickState, argVal: true });
230
+ KeyBinder.keyBinder.setKey('C-p', { thisArg: this, func: this.pickState, argVal: false }); // can't use Meta-P
231
+ KeyBinder.keyBinder.setKey('k', () => this.logWriter.showBacklog());
232
+ KeyBinder.keyBinder.setKey('D', () => this.fixit());
233
+ KeyBinder.keyBinder.setKey('C-s', () => {
234
+ blinkAndThen(this.hexMap.mapCont.markCont, () => this.gameSetup.restart({}));
235
+ });
236
+ // diagnostics:
237
+ table.undoShape.on(S.click, () => this.undoMove(), this);
238
+ table.redoShape.on(S.click, () => this.redoMove(), this);
239
+ }
240
+ /** enter debugger, with interesting values in local scope */
241
+ fixit() {
242
+ const table = this.table, player = this.curPlayer;
243
+ const hexMap = this.hexMap;
244
+ console.log(stime(this, `.fixit:`), { player, table, hexMap });
245
+ table.toggleText(true);
246
+ debugger;
247
+ return;
248
+ }
249
+ /** when turnNumber auto-increments. */
250
+ newTurn() {
251
+ }
252
+ readFileState() {
253
+ document.getElementById('fsReadFileButton')?.click();
254
+ }
255
+ // async fileState() {
256
+ // // Sadly, there is no way to suggest the filename for read?
257
+ // // I suppose we could do a openToWrite {suggestedName: ...} and accept the 'already exists'
258
+ // // seek to end, ...but not clear we could ever READ from the file handle.
259
+ // const turn = this.gameSetup.fileTurn;
260
+ // const [startelt, ...stateArray] = await this.gameSetup.injestFile(`log/${this.gameSetup.fileName}.js`, turn);
261
+ // const state = stateArray.find(state => state.turn === turn);
262
+ // this.backStates.length = this.nstate = 0;
263
+ // this.backStates.unshift(state);
264
+ // console.log(stime(this, `.fileState: logArray =\n`), stateArray);
265
+ // this.gameSetup.restart(state);
266
+ // }
267
+ backStates = [];
268
+ /** setNextPlayer->startTurn (or Key['p']) */
269
+ saveState() {
270
+ if (this.nstate !== 0) {
271
+ this.backStates = this.backStates.slice(this.nstate); // remove ejected states
272
+ this.nstate = 0;
273
+ }
274
+ const state = this.gameSetup.scenarioParser.saveState(this);
275
+ this.backStates.unshift(state);
276
+ console.log(stime(this, `.saveState -------- #${this.nstate}:${this.backStates.length - 1} turn=${state.turn}`), state);
277
+ }
278
+ // TODO: setup undo index to go fwd and back? wire into undoCont?
279
+ nstate = 0;
280
+ /** move nstate to older(back=true, S-P) or newer(back=false, C-P) states in backStates */
281
+ pickState(back = true) {
282
+ this.nstate = back ? Math.min(this.backStates.length - 1, this.nstate + 1) : Math.max(0, this.nstate - 1);
283
+ const state = this.backStates[this.nstate];
284
+ console.log(stime(this, `.pickState -------- #${this.nstate}:${this.backStates.length - 1} turn=${state.turn}:`), state);
285
+ this.gameSetup.parseScenenario(state); // typically sets gamePlay.turnNumber
286
+ console.log(stime(this, `.pickState -------- #${this.nstate}:${this.backStates.length - 1} turn=${state.turn}:`), state);
287
+ this.setNextPlayer(this.turnNumber);
288
+ }
289
+ clickDone() {
290
+ this.table.doneClicked({});
291
+ }
292
+ clickConfirm(val) {
293
+ this.curPlayer.panel.clickConfirm(val);
294
+ }
295
+ useReferee = true;
296
+ async waitPaused(p = this.curPlayer, ident = '') {
297
+ this.hexMap.update();
298
+ let isPaused = !p.planner.pauseP.resolved;
299
+ if (isPaused) {
300
+ console.log(stime(this, `.waitPaused: ${p.colorn} ${ident} waiting...`));
301
+ await p.planner?.waitPaused(ident);
302
+ console.log(stime(this, `.waitPaused: ${p.colorn} ${ident} running`));
303
+ }
304
+ this.hexMap.update();
305
+ }
306
+ pauseGame(p = this.curPlayer) {
307
+ p.planner?.pause();
308
+ this.hexMap.update();
309
+ console.log(stime(this, `.pauseGame: ${p.colorn}`));
310
+ }
311
+ resumeGame(p = this.curPlayer) {
312
+ p.planner?.resume();
313
+ this.hexMap.update();
314
+ console.log(stime(this, `.resumeGame: ${p.colorn}`));
315
+ }
316
+ /** tell [robo-]Player to stop thinking and make their Move; also set useRobo = false */
317
+ stopPlayer() {
318
+ this.autoMove(false);
319
+ this.curPlayer.stopMove();
320
+ console.log(stime(this, `.stopPlan:`), { planner: this.curPlayer.planner }, '----------------------');
321
+ setTimeout(() => { this.table.showWinText(`stopPlan`); }, 400);
322
+ }
323
+ /** undo and makeMove(incb=1) */
324
+ makeMoveAgain(arg, ev) {
325
+ if (this.curPlayer.plannerRunning)
326
+ return;
327
+ this.undoMove();
328
+ this.makeMove(true, undefined, 1);
329
+ }
330
+ cacheScale = TP.cacheTiles;
331
+ reCacheTiles() {
332
+ this.cacheScale = Math.max(1, this.table.scaleCont.scaleX);
333
+ TP.cacheTiles = (TP.cacheTiles == 0) ? this.cacheScale : 0;
334
+ console.log(stime('GamePlay', `.reCacheTiles: TP.cacheTiles=`), TP.cacheTiles, this.table.scaleCont.scaleX);
335
+ Tile.allTiles.forEach(tile => {
336
+ if (tile.cacheID) {
337
+ tile.uncache();
338
+ }
339
+ else {
340
+ const rad = tile.radius, b = tile.getBounds() ?? { x: -rad, y: -rad, width: 2 * rad, height: 2 * rad };
341
+ // tile.cache(b?.x ?? -rad, b?.y ?? -rad, b?.width ?? 2 * rad, b?.height ?? 2 * rad, TP.cacheTiles);
342
+ tile.cache(b.x, b.y, b.width, b.height, TP.cacheTiles);
343
+ }
344
+ });
345
+ this.hexMap.update();
346
+ }
347
+ /**
348
+ * Current Player takes action.
349
+ *
350
+ * after setNextPlayer: enable Player (GUI or Planner) to respond
351
+ * with playerMove() [table.moveStoneToHex()]
352
+ *
353
+ * Note: 1st move: player = otherPlayer(curPlayer)
354
+ * @param auto this.runRedo || undefined -> player.useRobo
355
+ * @param ev KeyBinder event, not used.
356
+ * @param incb increase Breadth of search
357
+ */
358
+ makeMove(auto, ev, incb = 0) {
359
+ let player = this.curPlayer;
360
+ if (this.runRedo) {
361
+ this.waitPaused(player, `.makeMove(runRedo)`).then(() => setTimeout(() => this.redoMove(), 10));
362
+ return;
363
+ }
364
+ if (auto === undefined)
365
+ auto = player.useRobo;
366
+ player.playerMove(auto, incb); // make one robo move
367
+ }
368
+ /** if useRobo == true, then Player delegates to robo-player immediately. */
369
+ autoMove(useRobo = false) {
370
+ this.forEachPlayer(p => {
371
+ this.roboPlay(p.index, useRobo);
372
+ });
373
+ }
374
+ autoPlay(pid = 0) {
375
+ this.roboPlay(pid, true); // KeyBinder uses arg2
376
+ if (this.curPlayerNdx == pid)
377
+ this.makeMove(true);
378
+ }
379
+ roboPlay(pid = 0, useRobo = true) {
380
+ let p = this.allPlayers[pid];
381
+ p.useRobo = useRobo;
382
+ console.log(stime(this, `.autoPlay: ${p.colorn}.useRobo=`), p.useRobo);
383
+ }
384
+ /** when true, run all the redoMoves. */
385
+ set runRedo(val) { (this._runRedo = val) && this.makeMove(); }
386
+ get runRedo() { return this.redoMoves.length > 0 ? this._runRedo : (this._runRedo = false); }
387
+ _runRedo = false;
388
+ /** invoked by GUI or Keyboard */
389
+ undoMove(undoTurn = true) {
390
+ this.table.stopDragging(); // drop on nextHex (no Move)
391
+ //
392
+ // undo state...
393
+ //
394
+ this.showRedoMark();
395
+ this.hexMap.update();
396
+ }
397
+ /** doTableMove(redoMoves[0]) */
398
+ redoMove() {
399
+ this.table.stopDragging(); // drop on nextHex (no Move)
400
+ let move = this.redoMoves[0]; // addStoneEvent will .shift() it off
401
+ if (!move)
402
+ return;
403
+ this.table.doTableMove(move.hex);
404
+ this.showRedoMark();
405
+ this.hexMap.update();
406
+ }
407
+ showRedoMark(hex = this.redoMoves[0]?.hex) {
408
+ if (!!hex) { // unless Skip or Resign...
409
+ this.hexMap.showMark((hex instanceof Hex) ? hex : Hex.ofMap(hex, this.hexMap));
410
+ }
411
+ }
412
+ endTurn() {
413
+ // vvvv maybe unnecessary: prompted by other confusion in save/restore:
414
+ // this.table.activateActionSelect(true, undefined); // resetToFirstButton() before newTurn->saveState.
415
+ super.endTurn();
416
+ }
417
+ setNextPlayer(turnNumber) {
418
+ this.curPlayer.panel.showPlayer(false);
419
+ super.setNextPlayer(turnNumber); // update player.coins
420
+ const fileName = this.gameSetup.logWriter.fileName;
421
+ const [logName, ext] = (fileName ?? this.gameSetup.logTime_js)?.split('.');
422
+ const backLog = this.logWriter.fileName ? '' : ' **';
423
+ const logAt = `${logName}@${this.turnNumber}${backLog}`;
424
+ this.logText(`&file=${logAt} ${this.curPlayer.Aname} ${stime.fs()}`, `GamePlay.setNextPlayer`);
425
+ ;
426
+ document.getElementById('readFileName').value = logAt;
427
+ this.curPlayer.panel.showPlayer(true);
428
+ this.paintForPlayer();
429
+ this.updateCounters(); // beginning of round...
430
+ this.curPlayer.panel.visible = true;
431
+ this.table.showNextPlayer(); // get to nextPlayer, waitPaused when Player tries to make a move.?
432
+ this.hexMap.update();
433
+ this.startTurn();
434
+ this.makeMove();
435
+ }
436
+ /** After setNextPlayer() */
437
+ startTurn() {
438
+ }
439
+ paintForPlayer() {
440
+ }
441
+ /** dropFunc | eval_sendMove -- indicating new Move attempt */
442
+ localMoveEvent(hev) {
443
+ let redo = this.redoMoves.shift(); // pop one Move, maybe pop them all:
444
+ //if (!!redo && redo.hex !== hev.hex) this.redoMoves.splice(0, this.redoMoves.length)
445
+ //this.doPlayerMove(hev.hex, hev.playerColor)
446
+ this.setNextPlayer();
447
+ this.ll(2) && console.log(stime(this, `.localMoveEvent: after doPlayerMove - setNextPlayer =`), this.curPlayer.color);
448
+ return false;
449
+ }
450
+ /** local Player has moved (S.add); network ? (sendMove.then(removeMoveEvent)) : localMoveEvent() */
451
+ playerMoveEvent(hev) {
452
+ this.localMoveEvent(hev);
453
+ return false;
454
+ }
455
+ }