@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
|
@@ -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
|
+
}
|