@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,280 @@
1
+ import { C, DropdownChoice, ParamGUI, blinkAndThen, makeStage, stime } from "@thegraid/easeljs-lib";
2
+ import { Container } from "@thegraid/easeljs-module";
3
+ import { parse as JSON5_parse } from 'json5';
4
+ import { EBC, PidChoice } from "./choosers";
5
+ import { GamePlay, NamedContainer } from "./game-play";
6
+ import { Meeple } from "./meeple";
7
+ import { Player } from "./player";
8
+ import { ScenarioParser } from "./scenario-parser";
9
+ import { RectShape } from "./shapes";
10
+ import { LogReader, LogWriter } from "./stream-writer";
11
+ import { Table } from "./table";
12
+ import { TP } from "./table-params";
13
+ import { Tile } from "./tile";
14
+ /** show " R" for " N" */
15
+ stime.anno = (obj) => {
16
+ let stage = (typeof obj !== 'string') ? (obj?.stage || obj?.table?.stage) : undefined;
17
+ return !!stage ? (!!stage.canvas ? " C" : " R") : " -";
18
+ };
19
+ ;
20
+ class MultiChoice extends DropdownChoice {
21
+ // constructor(items: MultiItem[], item_w: number, item_h: number, style?: DropdownStyle) {
22
+ // super(items, item_w, item_h, style);
23
+ // }
24
+ select(item) {
25
+ this.changed(item);
26
+ return item;
27
+ }
28
+ }
29
+ /** initialize & reset & startup the application/game. */
30
+ export class GameSetup {
31
+ qParams;
32
+ stage;
33
+ gamePlay;
34
+ paramGUIs;
35
+ netGUI; // paramGUIs[2]
36
+ /**
37
+ * ngAfterViewInit --> start here!
38
+ * @param canvasId supply undefined for 'headless' Stage
39
+ */
40
+ constructor(canvasId, qParams = []) {
41
+ this.qParams = qParams;
42
+ stime.fmt = "MM-DD kk:mm:ss.SSSL";
43
+ this.stage = makeStage(canvasId, false);
44
+ this.stage.snapToPixel = TP.snapToPixel;
45
+ this.setupToParseState(); // restart when/if 'SetState' button is clicked
46
+ this.setupToReadFileState(); // restart when/if 'LoadFile' button is clicked
47
+ Tile.loader.loadImages(() => this.startup(qParams));
48
+ }
49
+ /** set from qParams['n'] */
50
+ nPlayers = 2;
51
+ makeNplayers(gamePlay) {
52
+ // Create and Inject all the Players:
53
+ const allPlayers = gamePlay.allPlayers;
54
+ allPlayers.length = 0;
55
+ for (let ndx = 0; ndx < this.nPlayers; ndx++) {
56
+ new Player(ndx, gamePlay); // make real Players...
57
+ }
58
+ gamePlay.curPlayerNdx = 0; // gamePlay.setNextPlayer(0); ???
59
+ gamePlay.curPlayer = allPlayers[gamePlay.curPlayerNdx];
60
+ }
61
+ _netState = " "; // or "yes" or "ref"
62
+ set netState(val) {
63
+ this._netState = (val == "cnx") ? this._netState : val || " ";
64
+ this.gamePlay.ll(2) && console.log(stime(this, `.netState('${val}')->'${this._netState}'`));
65
+ this.netGUI?.selectValue("Network", val);
66
+ }
67
+ get netState() { return this._netState; }
68
+ set playerId(val) { this.netGUI?.selectValue("PlayerId", val || " "); }
69
+ logTime_js;
70
+ logWriter = this.makeLogWriter();
71
+ makeLogWriter() {
72
+ const logTime_js = this.logTime_js = `log_${stime.fs('MM-DD_Lkk_mm')}.js`;
73
+ const logWriter = new LogWriter(logTime_js, '[\n', ']\n'); // terminate array, but insert before terminal
74
+ return logWriter;
75
+ }
76
+ restartable = false;
77
+ /** C-s ==> kill game, start a new one, possibly with new dbp */
78
+ restart(stateInfo) {
79
+ if (!this.restartable)
80
+ return;
81
+ let netState = this.netState;
82
+ // this.gamePlay.closeNetwork('restart')
83
+ // this.gamePlay.logWriter?.closeFile()
84
+ this.gamePlay.forEachPlayer(p => p.endGame());
85
+ Tile.allTiles.forEach(tile => tile.hex = undefined);
86
+ let deContainer = (cont) => {
87
+ cont.children.forEach(dObj => {
88
+ dObj.removeAllEventListeners();
89
+ if (dObj instanceof Container)
90
+ deContainer(dObj);
91
+ });
92
+ cont.removeAllChildren();
93
+ };
94
+ deContainer(this.stage);
95
+ this.resetState(stateInfo);
96
+ // next tick, new thread...
97
+ setTimeout(() => this.netState = netState, 100); // onChange-> ("new", "join", "ref") initiate a new connection
98
+ }
99
+ /** override: invoked by restart(); with stateInfo JSON5_parse(stateText) */
100
+ resetState(stateInfo) {
101
+ const { mh, nh, hexRad } = stateInfo; // for example
102
+ TP.mHexes = mh ?? TP.mHexes;
103
+ TP.nHexes = nh ?? TP.nHexes;
104
+ TP.hexRad = hexRad ?? TP.hexRad;
105
+ this.startup();
106
+ }
107
+ /** read & parse State from text element */
108
+ setupToParseState() {
109
+ const parseStateButton = document.getElementById('parseStateButton');
110
+ const parseStateText = document.getElementById('parseStateText');
111
+ parseStateButton.onclick = () => {
112
+ const stateText = parseStateText.value;
113
+ const state = JSON5_parse(stateText);
114
+ state.Aname = state.Aname ?? `parseStateText`;
115
+ blinkAndThen(this.gamePlay.hexMap.mapCont.markCont, () => this.restart(state));
116
+ };
117
+ }
118
+ fileReadPromise;
119
+ async setupToReadFileState() {
120
+ const logReader = new LogReader(`log/date_time.js`, 'fsReadFileButton');
121
+ this.fileReadPromise = logReader.setButtonToReadFile();
122
+ const fileHandle = await this.fileReadPromise;
123
+ const fileText = await logReader.readFile(fileHandle);
124
+ const fullName = fileHandle.name;
125
+ const [fileName, ext] = fullName.split('.');
126
+ const readFileNameElt = document.getElementById('readFileName');
127
+ const readFileName = readFileNameElt.value;
128
+ const [fname, turnstr] = readFileName.split('@'); // fileName@turn
129
+ const turn = Number.parseInt(turnstr);
130
+ const state = this.extractStateFromString(fileName, fileText, turn);
131
+ this.setupToReadFileState(); // another thread to wait for next click
132
+ this.restart(state);
133
+ }
134
+ extractStateFromString(fileName, fileText, turn) {
135
+ const logArray = JSON5_parse(fileText);
136
+ const [, ...stateArray] = logArray;
137
+ const state = stateArray.find(state => state.turn === turn) ?? {};
138
+ state.Aname = `${fileName}@${turn}`;
139
+ return state;
140
+ }
141
+ /**
142
+ * Make new Table/layout & gamePlay/hexMap & Players.
143
+ * @param qParams from URL
144
+ */
145
+ startup(qParams = this.qParams) {
146
+ this.nPlayers = Math.min(TP.maxPlayers, qParams?.['n'] ? Number.parseInt(qParams?.['n']) : 2);
147
+ this.startScenario({ turn: 0, Aname: 'defaultScenario' });
148
+ }
149
+ /** scenario.turn indicate a FULL/SAVED scenario */
150
+ startScenario(scenario) {
151
+ Tile.allTiles = [];
152
+ Meeple.allMeeples = [];
153
+ Player.allPlayers = [];
154
+ const table = new Table(this.stage); // EventDispatcher, ScaleCont, GUI-Player
155
+ // Inject Table into GamePlay & make allPlayers:
156
+ const gamePlay = new GamePlay(scenario, table, this); // hexMap, players, fillBag, gStats, mouse/keyboard->GamePlay
157
+ this.gamePlay = gamePlay;
158
+ this.makeNplayers(gamePlay); // Players have: civics & meeples & TownSpec
159
+ // Inject GamePlay to Table; all the GUI components, makeAllDistricts(), addTerrain, initialRegions
160
+ table.layoutTable(gamePlay); // mutual injection & make all panelForPlayer
161
+ gamePlay.forEachPlayer(p => table.setPlayerScore(p, 0));
162
+ this.gamePlay.turnNumber = -1; // in prep for setNextPlayer or parseScenario
163
+ // Place Pieces and Figures on map:
164
+ this.parseScenenario(scenario); // may change gamePlay.turnNumber, gamePlay.phase (& conflictRegion)
165
+ this.gamePlay.logWriterLine0();
166
+ gamePlay.forEachPlayer(p => p.newGame(gamePlay)); // make Planner *after* table & gamePlay are setup
167
+ this.restartable = false;
168
+ this.makeGUIs(table);
169
+ this.restartable = true; // *after* makeLines has stablilized selectValue
170
+ table.startGame(scenario); // parseScenario; allTiles.makeDragable(); setNextPlayer();
171
+ return gamePlay;
172
+ }
173
+ makeGUIs(table) {
174
+ const scaleCont = table.scaleCont, scale = TP.hexRad / 60, cx = -200, cy = 250, d = 5;
175
+ // this.makeParamGUI(table.scaleCont, -400, 250);
176
+ const gpanel = (makeGUI, name, cx, cy, scale = 1) => {
177
+ const guiC = new NamedContainer(name, cx * scale, cy * scale);
178
+ // const map = table.hexMap.mapCont.parent;
179
+ scaleCont.addChildAt(guiC);
180
+ guiC.scaleX = guiC.scaleY = scale;
181
+ const gui = makeGUI.call(this, guiC); // @[0, 0]
182
+ guiC.x -= (gui.linew + d) * scale;
183
+ const bgr = new RectShape({ x: -d, y: -d, w: gui.linew + 2 * d, h: gui.ymax + 2 * d }, 'rgb(200,200,200,.5)', '');
184
+ guiC.addChildAt(bgr, 0);
185
+ table.dragger.makeDragable(guiC);
186
+ return gui;
187
+ };
188
+ let ymax = 0;
189
+ const gui3 = gpanel(this.makeNetworkGUI, 'NetGUI', cx, cy + ymax, scale);
190
+ ymax += gui3.ymax + 20;
191
+ const gui1 = gpanel(this.makeParamGUI, 'ParamGUI', cx, cy + ymax, scale);
192
+ ymax += gui1.ymax + 20;
193
+ const gui2 = gpanel(this.makeParamGUI2, 'AI_GUI', cx, cy + ymax, scale);
194
+ ymax += gui2.ymax + 20;
195
+ scaleCont.addChild(gui2.parent, gui1.parent, gui3.parent); // lower y values ABOVE to dropdown is not obscured
196
+ // TODO: dropdown to use given 'top' container!
197
+ gui1.stage.update();
198
+ }
199
+ scenarioParser;
200
+ parseScenenario(scenario) {
201
+ const hexMap = this.gamePlay.hexMap;
202
+ const scenarioParser = this.scenarioParser = new ScenarioParser(hexMap, this.gamePlay);
203
+ this.gamePlay.logWriter.writeLine(`// GameSetup.parseScenario: ${scenario.Aname}`);
204
+ scenarioParser.parseScenario(scenario);
205
+ }
206
+ /** affects the rules of the game & board
207
+ *
208
+ * ParamGUI --> board & rules [under stats panel]
209
+ * ParamGUI2 --> AI Player [left of ParamGUI]
210
+ * NetworkGUI --> network [below ParamGUI2]
211
+ */
212
+ makeParamGUI(parent, x = 0, y = 0) {
213
+ const gui = new ParamGUI(TP, { textAlign: 'right' });
214
+ gui.makeParamSpec('hexRad', [30, 60, 90, 120], { fontColor: 'red' });
215
+ TP.hexRad;
216
+ gui.makeParamSpec('nHexes', [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], { fontColor: 'red' });
217
+ TP.nHexes;
218
+ gui.makeParamSpec('mHexes', [1, 2, 3], { fontColor: 'red' });
219
+ TP.mHexes;
220
+ gui.spec("hexRad").onChange = (item) => { this.restart({ hexRad: item.value }); };
221
+ gui.spec("nHexes").onChange = (item) => { this.restart({ nh: item.value }); };
222
+ gui.spec("mHexes").onChange = (item) => { this.restart({ mh: item.value }); };
223
+ parent.addChild(gui);
224
+ gui.x = x; // (3*cw+1*ch+6*m) + max(line.width) - (max(choser.width) + 20)
225
+ gui.y = y;
226
+ gui.makeLines();
227
+ return gui;
228
+ }
229
+ /** configures the AI player */
230
+ makeParamGUI2(parent, x = 0, y = 0) {
231
+ const gui = new ParamGUI(TP, { textAlign: 'center' });
232
+ gui.makeParamSpec("log", [-1, 0, 1, 2], { style: { textAlign: 'right' } });
233
+ TP.log;
234
+ gui.makeParamSpec("maxPlys", [1, 2, 3, 4, 5, 6, 7, 8], { fontColor: "blue" });
235
+ TP.maxPlys;
236
+ gui.makeParamSpec("maxBreadth", [5, 6, 7, 8, 9, 10], { fontColor: "blue" });
237
+ TP.maxBreadth;
238
+ parent.addChild(gui);
239
+ gui.x = x;
240
+ gui.y = y;
241
+ gui.makeLines();
242
+ gui.stage.update();
243
+ return gui;
244
+ }
245
+ netColor = "rgba(160,160,160, .8)";
246
+ netStyle = { textAlign: 'right' };
247
+ /** controls multiplayer network participation */
248
+ makeNetworkGUI(parent, x = 0, y = 0) {
249
+ const gui = this.netGUI = new ParamGUI(TP, this.netStyle);
250
+ gui.makeParamSpec("Network", [" ", "new", "join", "no", "ref", "cnx"], { fontColor: "red" });
251
+ gui.makeParamSpec("PlayerId", [" ", 0, 1, 2, 3, "ref"], { chooser: PidChoice, fontColor: "red" });
252
+ gui.makeParamSpec("networkGroup", [TP.networkGroup], { chooser: EBC, name: 'gid', fontColor: C.GREEN, style: { textColor: C.BLACK } });
253
+ TP.networkGroup;
254
+ gui.spec("Network").onChange = (item) => {
255
+ if (['new', 'join', 'ref'].includes(item.value)) {
256
+ const group = gui.findLine('networkGroup').chooser.editBox.innerText;
257
+ // this.gamePlay.closeNetwork()
258
+ // this.gamePlay.network(item.value, gui, group)
259
+ }
260
+ // if (item.value === "no") this.gamePlay.closeNetwork() // provoked by ckey
261
+ };
262
+ this.stage.canvas?.parentElement?.addEventListener('paste', (ev) => {
263
+ const text = ev.clipboardData?.getData('Text');
264
+ ;
265
+ gui.findLine('networkGroup').chooser.setValue(text);
266
+ });
267
+ this.showNetworkGroup();
268
+ parent.addChild(gui);
269
+ gui.makeLines();
270
+ gui.x = x;
271
+ gui.y = y;
272
+ parent.stage.update();
273
+ return gui;
274
+ }
275
+ showNetworkGroup(group_name = TP.networkGroup) {
276
+ document.getElementById('group_name').innerText = group_name;
277
+ const line = this.netGUI.findLine("networkGroup"), chooser = line?.chooser;
278
+ chooser?.setValue(group_name, chooser.items[0], undefined);
279
+ }
280
+ }
@@ -0,0 +1,112 @@
1
+ import { stime } from "@thegraid/common-lib";
2
+ export class GameState {
3
+ gamePlay;
4
+ constructor(gamePlay) {
5
+ this.gamePlay = gamePlay;
6
+ Object.keys(this.states).forEach((key) => this.states[key].Aname = key);
7
+ }
8
+ state;
9
+ get table() { return this.gamePlay?.table; }
10
+ get curPlayer() { return this.gamePlay.curPlayer; }
11
+ saveGame() {
12
+ this.gamePlay.gameSetup.scenarioParser.saveState(this.gamePlay);
13
+ }
14
+ // [eventName, eventSpecial, phase, args]
15
+ saveState() {
16
+ }
17
+ parseState(args) {
18
+ }
19
+ startPhase = 'BeginTurn';
20
+ startArgs = [];
21
+ /** Bootstrap the Scenario: set bastetPlayer and then this.phase(startPhase, ...startArgs). */
22
+ start() {
23
+ this.phase(this.startPhase, ...this.startArgs);
24
+ }
25
+ /** set state and start with given args. */
26
+ phase(phase, ...args) {
27
+ console.log(stime(this, `.phase: ${this.state?.Aname ?? 'Initialize'} -> ${phase}`));
28
+ this.state = this.states[phase];
29
+ this.state.start(...args);
30
+ }
31
+ /** set label & paint button with color;
32
+ * empty label hides & disables.
33
+ * optional continuation function on 'drawend'.
34
+ */
35
+ doneButton(label, color = this.curPlayer.color, afterUpdate = undefined) {
36
+ const doneButton = this.table.doneButton;
37
+ doneButton.visible = !!label;
38
+ doneButton.label_text = label;
39
+ doneButton.paint(color, true);
40
+ doneButton.updateWait(false, afterUpdate);
41
+ }
42
+ /** invoked when 'Done' button clicked. [or whenever phase is 'done' by other means] */
43
+ done(...args) {
44
+ (this.state.done ?? ((...args) => { alert('no done method'); }))(...args);
45
+ }
46
+ undoAction() {
47
+ // const action = this.selectedAction;
48
+ // if (!action) return;
49
+ // this.states[action].undo?.();
50
+ }
51
+ states = {
52
+ BeginTurn: {
53
+ start: () => {
54
+ this.saveGame();
55
+ this.phase('ChooseAction');
56
+ },
57
+ done: () => {
58
+ this.phase('ChooseAction');
59
+ }
60
+ },
61
+ Move: {
62
+ start: () => {
63
+ this.doneButton('Move done');
64
+ },
65
+ done: (ok) => {
66
+ this.phase('EndAction');
67
+ },
68
+ },
69
+ Summon: {
70
+ start: () => {
71
+ this.doneButton('Summon done');
72
+ },
73
+ done: () => {
74
+ this.phase('EndAction');
75
+ },
76
+ },
77
+ EndAction: {
78
+ nextPhase: 'ChooseAction',
79
+ start: () => {
80
+ const nextPhase = this.state.nextPhase = 'Event';
81
+ this.phase(nextPhase); // directl -> nextPhase
82
+ },
83
+ done: () => {
84
+ this.phase(this.state.nextPhase ?? 'Start'); // TS want defined...
85
+ }
86
+ },
87
+ Conflict: {
88
+ start: () => {
89
+ },
90
+ },
91
+ ConflictRegionDone: {
92
+ start: () => {
93
+ this.phase('ConflictNextRegion');
94
+ }
95
+ },
96
+ ConflictDone: {
97
+ start: () => {
98
+ this.phase('EventDone');
99
+ },
100
+ // TODO: coins from Scales to Toth, add Devotion(Scales)
101
+ },
102
+ EndTurn: {
103
+ start: () => {
104
+ this.gamePlay.endTurn();
105
+ this.phase('BeginTurn');
106
+ },
107
+ },
108
+ /** Hathor: after addFollowers() Ankh-Event, BuildMonument, Worshipful, Summon-AnubisRansom */
109
+ };
110
+ setup() {
111
+ }
112
+ }
@@ -0,0 +1,82 @@
1
+ /** Hexagonal canonical directions */
2
+ export var Dir;
3
+ (function (Dir) {
4
+ Dir[Dir["C"] = 0] = "C";
5
+ Dir[Dir["NE"] = 1] = "NE";
6
+ Dir[Dir["E"] = 2] = "E";
7
+ Dir[Dir["SE"] = 3] = "SE";
8
+ Dir[Dir["SW"] = 4] = "SW";
9
+ Dir[Dir["W"] = 5] = "W";
10
+ Dir[Dir["NW"] = 6] = "NW";
11
+ })(Dir || (Dir = {}));
12
+ /** Hex things */
13
+ export var H;
14
+ (function (H) {
15
+ H.degToRadians = Math.PI / 180;
16
+ H.sqrt3 = Math.sqrt(3); // 1.7320508075688772
17
+ H.sqrt3_2 = H.sqrt3 / 2;
18
+ H.infin = String.fromCodePoint(0x221E);
19
+ H.C = "C"; // not a HexDir, but identifies a Center
20
+ H.N = "N";
21
+ H.S = "S";
22
+ H.E = "E";
23
+ H.W = "W";
24
+ H.NE = "NE";
25
+ H.SE = "SE";
26
+ H.SW = "SW";
27
+ H.NW = "NW";
28
+ H.EN = "EN";
29
+ H.ES = "ES";
30
+ H.WS = "WS";
31
+ H.WN = "WN";
32
+ function hexBounds(r, tilt = 0) {
33
+ // dp(...6), so tilt: 30 | 0; being nsAxis (ewTopo) or ewAxis (nsTopo);
34
+ const w = r * Math.cos(H.degToRadians * tilt);
35
+ const h = r * Math.cos(H.degToRadians * (tilt - 30));
36
+ return { x: -w, y: -h, width: 2 * w, height: 2 * h };
37
+ }
38
+ H.hexBounds = hexBounds;
39
+ /** neighborhood topology, E-W & N-S orientation; even(n0) & odd(n1) rows: */
40
+ H.ewEvenRow = {
41
+ NE: { dc: 0, dr: -1 }, E: { dc: 1, dr: 0 }, SE: { dc: 0, dr: 1 },
42
+ SW: { dc: -1, dr: 1 }, W: { dc: -1, dr: 0 }, NW: { dc: -1, dr: -1 }
43
+ };
44
+ H.ewOddRow = {
45
+ NE: { dc: 1, dr: -1 }, E: { dc: 1, dr: 0 }, SE: { dc: 1, dr: 1 },
46
+ SW: { dc: 0, dr: 1 }, W: { dc: -1, dr: 0 }, NW: { dc: 0, dr: -1 }
47
+ };
48
+ H.nsEvenCol = {
49
+ EN: { dc: +1, dr: -1 }, N: { dc: 0, dr: -1 }, ES: { dc: +1, dr: 0 },
50
+ WS: { dc: -1, dr: 0 }, S: { dc: 0, dr: +1 }, WN: { dc: -1, dr: -1 }
51
+ };
52
+ H.nsOddCol = {
53
+ EN: { dc: 1, dr: 0 }, N: { dc: 0, dr: -1 }, ES: { dc: 1, dr: 1 },
54
+ WS: { dc: -1, dr: 1 }, S: { dc: 0, dr: 1 }, WN: { dc: -1, dr: 0 }
55
+ };
56
+ function nsTopo(rc) { return (rc.col % 2 == 0) ? H.nsEvenCol : H.nsOddCol; }
57
+ H.nsTopo = nsTopo;
58
+ ;
59
+ function ewTopo(rc) { return (rc.row % 2 == 0) ? H.ewEvenRow : H.ewOddRow; }
60
+ H.ewTopo = ewTopo;
61
+ ;
62
+ /** includes E & W, suitable for EwTopo */
63
+ H.ewDirs = [H.NE, H.E, H.SE, H.SW, H.W, H.NW]; // directions for EwTOPO
64
+ /** includes N & S, suitable for NsTopo */
65
+ H.nsDirs = [H.N, H.EN, H.ES, H.S, H.WS, H.WN]; // directions for NsTOPO
66
+ /** all hexDirs */
67
+ H.hexDirs = H.ewDirs.concat(H.nsDirs); // standard direction signifiers () ClockWise
68
+ // angles for ewTopo!
69
+ H.ewDirRot = { NE: 30, E: 90, SE: 150, SW: 210, W: 270, NW: 330 };
70
+ // angles for nwTopo!
71
+ H.nsDirRot = { N: 0, EN: 60, ES: 120, S: 180, WS: 240, WN: 300 };
72
+ H.dirRot = { ...H.ewDirRot, ...H.nsDirRot };
73
+ H.dirRev = { N: H.S, S: H.N, E: H.W, W: H.E, NE: H.SW, SE: H.NW, SW: H.NE, NW: H.SE, ES: H.WN, EN: H.WS, WS: H.EN, WN: H.ES };
74
+ H.dirRevEW = { E: H.W, W: H.E, NE: H.SW, SE: H.NW, SW: H.NE, NW: H.SE };
75
+ H.dirRevNS = { N: H.S, S: H.N, EN: H.WS, ES: H.WN, WS: H.EN, WN: H.ES };
76
+ H.rotDir = { 0: 'N', 30: 'NE', 60: 'EN', 90: 'E', 120: 'ES', 150: 'SE', 180: 'S', 210: 'SW', 240: 'WS', 270: 'W', 300: 'WN', 330: 'NW', 360: 'N' };
77
+ H.capColor1 = "rgba(150, 0, 0, .8)"; // unplayable: captured last turn
78
+ H.capColor2 = "rgba(128, 80, 80, .8)"; // protoMove would capture
79
+ H.sacColor1 = "rgba(228, 80, 0, .8)"; // unplayable: sacrifice w/o capture
80
+ H.sacColor2 = "rgba(228, 120, 0, .6)"; // isplayable: sacrifice w/ capture
81
+ H.fjColor = "rgba(228, 228, 0, .8)"; // ~unplayable: jeopardy w/o capture
82
+ })(H || (H = {}));