@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.
- package/dist/choosers.js +59 -0
- package/dist/counters.js +155 -0
- package/dist/game-play.js +455 -0
- package/dist/game-setup.js +280 -0
- package/dist/game-state.js +112 -0
- package/dist/hex-intfs.js +82 -0
- package/dist/hex.js +714 -0
- package/dist/image-loader.js +69 -0
- package/dist/meeple.js +152 -0
- package/dist/plan-proxy.js +33 -0
- package/dist/player-panel.js +125 -0
- package/dist/player.js +112 -0
- package/dist/scenario-parser.js +67 -0
- package/dist/shapes.js +304 -0
- package/dist/stream-writer.js +192 -0
- package/dist/table-params.js +48 -0
- package/dist/table.js +707 -0
- package/dist/text-log.js +53 -0
- package/dist/tile-source.js +115 -0
- package/dist/tile.js +312 -0
- package/dist/types.js +15 -0
- package/package.json +45 -0
package/dist/choosers.js
ADDED
|
@@ -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
|
+
}
|
package/dist/counters.js
ADDED
|
@@ -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
|
+
}
|