Skip to content

Commit 5300702

Browse files
committed
Modular Tintas ready for live testing
1 parent 1cc9fec commit 5300702

26 files changed

+877
-73
lines changed

locales/en/apgames.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2567,6 +2567,12 @@
25672567
"description": "This variant expands the capturing options, leading to a more aggressive game."
25682568
}
25692569
},
2570+
"tintas": {
2571+
"modular": {
2572+
"name": "Modular board (7 cells, 49 spaces)",
2573+
"description": "Modular boards are composed of a certain number of 7-cell modules (hexhex 2s) distributed randomly but ensuring that each module touches another at two points."
2574+
}
2575+
},
25702576
"trax": {
25712577
"loop": {
25722578
"name": "Loop Trax",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"scripts": {
88
"build": "npm run json2ts && npm run build-ts && npm run lint",
99
"build-ts": "tsc && npm pack",
10-
"test0": "mocha -r ts-node/register test/games/wunchunk.test.ts",
10+
"test0": "mocha -r ts-node/register test/common/modular-board.test.ts",
1111
"test": "mocha -r ts-node/register test/**/*.test.ts",
1212
"lint": "npx eslint .",
1313
"dist-dev": "rimraf dist && webpack",

src/common/hexes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,6 @@ export const generateField = (numModules: number): IHexCoord[] => {
380380
];
381381
}
382382

383-
384383
const selectCtr = (ctrs: Set<string>, filled: Set<string>): [number,number] => {
385384
const chooseFrom = shuffle([...ctrs.values()].map(c => c.split(",").map(n => parseInt(n, 10)))) as [number,number][];
386385
// for each known ctr, select one at random until one works

src/common/modular/board.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { Direction, Grid, Orientation, rectangle, type HexOffset } from "honeycomb-grid";
2+
import { hexNeighbours } from "../../common/hexes";
3+
import { ModularGraph } from "./graph";
4+
import { createModularHex, ModularHex, type HexArgs } from "./hex";
5+
6+
type BoardArgs = {
7+
centres?: {q: number; r: number}[];
8+
hexes?: {q: number; r: number}[];
9+
orientation?: Orientation;
10+
offset?: HexOffset;
11+
};
12+
type AddArgs = HexArgs & {overwrite?: boolean};
13+
14+
export class ModularBoard {
15+
private _minX: number|undefined;
16+
private _maxX: number|undefined;
17+
private _minY: number|undefined;
18+
private _maxY: number|undefined;
19+
private _shiftX = 0;
20+
private _shiftY = 0;
21+
private _graph!: ModularGraph;
22+
private orientation: Orientation;
23+
private offset: HexOffset;
24+
private hexClass: ReturnType<typeof createModularHex>;
25+
26+
// _axial2hex is the "authoritative" source
27+
private _axial2hex: Map<string, ModularHex> = new Map();
28+
private _offset2hex: Map<string, ModularHex> = new Map();
29+
private _algebraic2hex: Map<string, ModularHex> = new Map();
30+
31+
constructor(args?: BoardArgs) {
32+
this.orientation = args?.orientation ?? Orientation.POINTY;
33+
this.offset = args?.offset ?? 1;
34+
this.hexClass = createModularHex(this.orientation, this.offset);
35+
// populate from given centre points if requested
36+
if (args?.centres !== undefined) {
37+
for (const {q, r} of args.centres) {
38+
const ctr = this.add({q, r, overwrite: true});
39+
for (const {q: nq, r: nr} of hexNeighbours(ctr)) {
40+
this.add({q: nq, r: nr, overwrite: true});
41+
}
42+
}
43+
}
44+
if (args?.hexes !== undefined) {
45+
for (const {q, r} of args.hexes) {
46+
this.add({q, r, overwrite: true});
47+
}
48+
}
49+
this.indexHexes();
50+
}
51+
52+
private indexHexes() {
53+
// Determine shifts to normalize coordinates while preserving parity
54+
// This ensures that the grid structure (odd/even rows/cols) remains consistent
55+
// with the fixed offset/orientation.
56+
this._shiftX = 0;
57+
this._shiftY = 0;
58+
59+
if (this.orientation === Orientation.POINTY) {
60+
if (this._minY !== undefined) {
61+
// For pointy, row parity matters.
62+
// If minY is odd, shift by minY - 1 (even amount) to keep it odd (1).
63+
// If minY is even, shift by minY (even amount) to keep it even (0).
64+
this._shiftY = this._minY % 2 !== 0 ? this._minY - 1 : this._minY;
65+
this._shiftX = this._minX ?? 0;
66+
}
67+
} else {
68+
if (this._minX !== undefined) {
69+
// For flat, col parity matters.
70+
this._shiftX = this._minX % 2 !== 0 ? this._minX - 1 : this._minX;
71+
this._shiftY = this._minY ?? 0;
72+
}
73+
}
74+
75+
// this gets called on an empty object when deserializing/cloning
76+
// so just skip it all if empty
77+
if (this._axial2hex.size > 0) {
78+
this._graph = new ModularGraph(this.width, this.height, this.orientation, this.offset);
79+
this._algebraic2hex.clear();
80+
// now link to algebraic coordinates, which aren't known until the board is fully populated
81+
this.hexes.forEach(hex => {
82+
this._algebraic2hex.set(this.hex2algebraic(hex), hex)
83+
});
84+
}
85+
}
86+
87+
// private because it should only be used by the constructor the one time
88+
private add(args: AddArgs): ModularHex {
89+
let overwrite = false;
90+
if (args.overwrite !== undefined) {
91+
overwrite = args.overwrite;
92+
}
93+
// no duplicates
94+
const newhex = this.hexClass.create(args);
95+
const found = this._axial2hex.get(`${args.q},${args.r}`);
96+
if (found && !overwrite) {
97+
throw new Error(`A hex at ${args.q},${args.r} already exists.`);
98+
}
99+
this._axial2hex.set(`${args.q},${args.r}`, newhex);
100+
this._offset2hex.set(`${newhex.col},${newhex.row}`, newhex);
101+
if (this._minX === undefined) {
102+
this._minX = newhex.col;
103+
} else {
104+
this._minX = Math.min(this._minX, newhex.col);
105+
}
106+
if (this._maxX === undefined) {
107+
this._maxX = newhex.col;
108+
} else {
109+
this._maxX = Math.max(this._maxX, newhex.col);
110+
}
111+
if (this._minY === undefined) {
112+
this._minY = newhex.row;
113+
} else {
114+
this._minY = Math.min(this._minY, newhex.row);
115+
}
116+
if (this._maxY === undefined) {
117+
this._maxY = newhex.row;
118+
} else {
119+
this._maxY = Math.max(this._maxY, newhex.row);
120+
}
121+
return newhex;
122+
}
123+
124+
public getHexAtOffset(col: number, row: number): ModularHex|undefined {
125+
const found = this._offset2hex.get(`${col},${row}`);
126+
// const found = this._hexes.find(h => h.col === col && h.row === row);
127+
if (found !== undefined) {
128+
return found.dupe();
129+
} else {
130+
return undefined;
131+
}
132+
}
133+
134+
public getHexAtAxial(q: number, r: number): ModularHex|undefined {
135+
const found = this._axial2hex.get(`${q},${r}`);
136+
// const found = this._hexes.find(h => h.q === q && h.r === r);
137+
if (found !== undefined) {
138+
return found.dupe();
139+
} else {
140+
return undefined;
141+
}
142+
}
143+
144+
public getHexAtAlgebraic(cell: string): ModularHex|undefined {
145+
const found = this._algebraic2hex.get(cell);
146+
if (found !== undefined) {
147+
return found.dupe();
148+
} else {
149+
return undefined;
150+
}
151+
// const [relx, rely] = this.graph.algebraic2coords(cell);
152+
// const absCol = this.minX + relx;
153+
// const absRow = this.minY + rely;
154+
// return this.getHexAtOffset(absCol, absRow);
155+
}
156+
157+
public get hexes(): ModularHex[] {
158+
return [...this._axial2hex.values()].map(h => h.dupe());
159+
}
160+
161+
public get minX(): number|undefined {
162+
return this._minX;
163+
}
164+
165+
public get maxX(): number|undefined {
166+
return this._maxX;
167+
}
168+
169+
public get minY(): number|undefined {
170+
return this._minY;
171+
}
172+
173+
public get maxY(): number|undefined {
174+
return this._maxY;
175+
}
176+
177+
public get height(): number {
178+
if (this.maxY === undefined || this.minY === undefined) {
179+
return 0;
180+
}
181+
return this.maxY - this._shiftY + 1;
182+
}
183+
184+
public get width(): number {
185+
if (this.maxX === undefined || this.minX === undefined) {
186+
return 0;
187+
}
188+
return this.maxX - this._shiftX + 1;
189+
}
190+
191+
public get graph(): ModularGraph {
192+
return this._graph;
193+
}
194+
195+
public hex2algebraic(hex: ModularHex): string {
196+
return this.graph.coords2algebraic(...this.hex2coords(hex));
197+
}
198+
199+
public hex2coords(hex: ModularHex): [number,number] {
200+
return [hex.col - this._shiftX, hex.row - this._shiftY];
201+
}
202+
203+
public clone(): ModularBoard {
204+
const cloned = new ModularBoard({orientation: this.orientation});
205+
this.hexes.forEach(h => cloned.add(h));
206+
cloned.indexHexes();
207+
return cloned;
208+
}
209+
210+
public serialize(): ModularHex[] {
211+
return this.hexes;
212+
}
213+
214+
public static deserialize(hexes: ModularHex[]): ModularBoard {
215+
const orientation = hexes.length > 0 ? hexes[0].orientation : Orientation.FLAT;
216+
const cloned = new ModularBoard({orientation});
217+
hexes.forEach(h => cloned.add(h));
218+
cloned.indexHexes();
219+
return cloned;
220+
}
221+
222+
public get hexesOrdered(): ModularHex[][] {
223+
const cells = this.graph.listCells(true) as string[][];
224+
const ordered: ModularHex[][] = [];
225+
for (const row of cells) {
226+
const realrow: ModularHex[] = [];
227+
for (const cell of row) {
228+
const hex = this.getHexAtAlgebraic(cell);
229+
if (hex !== undefined) {
230+
realrow.push(hex.dupe());
231+
}
232+
}
233+
ordered.push(realrow);
234+
}
235+
return ordered;
236+
}
237+
238+
public get grid(): Grid<ModularHex> {
239+
return new Grid(this.hexClass, rectangle({width: this.width, height: this.height}));
240+
}
241+
242+
public castRay(from: string, dir: Direction, opts: { ignoreVoids?: boolean } = {}): string[] {
243+
const ray: string[] = [];
244+
// Use the graph's ray function which is bounded by the board dimensions
245+
const potentialRay = this.graph.ray(from, dir);
246+
for (const cell of potentialRay) {
247+
if (this.getHexAtAlgebraic(cell) !== undefined) {
248+
ray.push(cell);
249+
} else if (!opts.ignoreVoids) {
250+
break;
251+
}
252+
}
253+
return ray;
254+
}
255+
256+
public get blockedCells(): string[] {
257+
const blocked: string[] = [];
258+
for (let x = 0; x < this.width; x++) {
259+
for (let y = 0; y < this.height; y++) {
260+
const cell = this.graph.coords2algebraic(x, y);
261+
if (this.getHexAtAlgebraic(cell) === undefined) {
262+
blocked.push(cell);
263+
}
264+
}
265+
}
266+
return blocked;
267+
}
268+
}

src/common/modular/graph.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { HexFieldGraph } from "../../common/graphs";
2+
import { Orientation, type HexOffset } from "honeycomb-grid";
3+
4+
const columnLabels = "abcdefghijklmnopqrstuvwxyz".split("");
5+
6+
export class ModularGraph extends HexFieldGraph {
7+
public orientation: Orientation;
8+
public offset: HexOffset;
9+
10+
constructor(width: number, height: number, orientation: Orientation, offset: HexOffset) {
11+
super(width, height, orientation, offset);
12+
this.orientation = orientation;
13+
this.offset = offset;
14+
}
15+
16+
public override coords2algebraic(x: number, y: number): string {
17+
let label = "";
18+
let idx = this.height - 1 - y;
19+
while (idx >= 0) {
20+
label = columnLabels[idx % 26] + label;
21+
idx = Math.floor(idx / 26) - 1;
22+
}
23+
return label + (x + 1).toString();
24+
}
25+
26+
public override algebraic2coords(cell: string): [number, number] {
27+
const nidx = cell.search(/\d/);
28+
if (nidx < 0) {
29+
throw new Error(`Could not find an digit in the cell '${cell}'.`);
30+
}
31+
const letters = cell.substring(0, nidx);
32+
const num = cell.substring(nidx);
33+
let y = 0;
34+
for (let i = 0; i < letters.length; i++) {
35+
const char = letters[i];
36+
const val = columnLabels.indexOf(char);
37+
if (val < 0) {
38+
throw new Error(`The column label is invalid: ${letters}`);
39+
}
40+
y = y * 26 + (val + 1);
41+
}
42+
y -= 1;
43+
if (y < 0) {
44+
throw new Error(`The column label is invalid: ${letters}`);
45+
}
46+
const x = Number(num);
47+
if (x === undefined || isNaN(x) || num === "") {
48+
throw new Error(`The row label is invalid: ${num}`);
49+
}
50+
// return [x - 1, y];
51+
return [x - 1, this.height - 1 - y];
52+
}
53+
}

src/common/modular/hex.test.ts

Whitespace-only changes.

src/common/modular/hex.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { defineHex, Orientation, Hex, type HexOffset } from "honeycomb-grid";
2+
3+
export type HexArgs = {q: number; r: number};
4+
5+
export interface ModularHex extends Hex {
6+
uid: string;
7+
col: number;
8+
row: number;
9+
dupe(): ModularHex;
10+
}
11+
12+
export const createModularHex = (orientation: Orientation = Orientation.FLAT, offset: HexOffset = 1) => {
13+
return class ModularHexImpl extends defineHex({ offset, orientation }) implements ModularHex {
14+
public get uid(): string {
15+
return `${this.q},${this.r}`;
16+
}
17+
18+
public get col(): number {
19+
if (orientation === Orientation.POINTY) {
20+
return this.q + (this.r + offset * (this.r & 1)) / 2;
21+
}
22+
return this.q;
23+
}
24+
25+
public get row(): number {
26+
if (orientation === Orientation.POINTY) {
27+
return this.r;
28+
}
29+
return this.r + (this.q + offset * (this.q & 1)) / 2;
30+
}
31+
32+
static create(args: HexArgs): ModularHex {
33+
return new ModularHexImpl({q: args.q, r: args.r});
34+
}
35+
36+
public dupe(): ModularHex {
37+
return ModularHexImpl.create({q: this.q, r: this.r});
38+
}
39+
40+
public static deserialize(hex: ModularHex): ModularHex {
41+
return ModularHexImpl.create({q: hex.q, r: hex.r});
42+
}
43+
};
44+
}

0 commit comments

Comments
 (0)