Skip to content

Commit 895b3e5

Browse files
authored
Stibro fix (#245)
Compute values on the fly
1 parent 866701d commit 895b3e5

File tree

3 files changed

+114
-176
lines changed

3 files changed

+114
-176
lines changed

locales/en/apgames.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,6 +2010,9 @@
20102010
}
20112011
},
20122012
"stibro": {
2013+
"size-5": {
2014+
"name": "Size 5 board"
2015+
},
20132016
"size-6": {
20142017
"name": "Size 6 board"
20152018
},

src/games/stibro.ts

Lines changed: 73 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APRenderRep, RowCol } from "@abstractplay/renderer/src/schemas/schema";
44
import { APMoveResult } from "../schemas/moveresults";
55
import { HexTriGraph, reviver, UserFacingError, StackSet } from "../common";
66
import { bfsFromNode, dfsFromNode } from 'graphology-traversal';
7+
import { connectedComponents } from 'graphology-components';
78
import i18next from "i18next";
89

910

@@ -15,8 +16,6 @@ export interface IMoveState extends IIndividualState {
1516
board: Map<string, cellcontent>;
1617
lastmove?: string;
1718
winningLoop: string[];
18-
groups: Map<playerid, Map<number, Set<string>>>;
19-
distantGroups: Set<Map<playerid, number>>;
2019
}
2120

2221
export interface IStibroState extends IAPGameState {
@@ -52,6 +51,7 @@ export class StibroGame extends GameBase {
5251
flags: ["pie"],
5352
categories: ["goal>connect", "mechanic>place", "board>shape>hex", "board>connect>hex", "components>simple>1per"],
5453
variants: [
54+
{uid: "size-5", group: "board"},
5555
{uid: "size-6", group: "board"},
5656
{uid: "#board", },
5757
{uid: "size-8", group: "board"}
@@ -79,29 +79,10 @@ export class StibroGame extends GameBase {
7979
(a) does not touch the edge;
8080
(b) has at least two cells between itself and at least one opponent group that does
8181
not touch the edge.
82-
83-
Keep track of groups, including whether they are free or not. Recomputing the free groups and
84-
their distances each time could get quite expensive, otherwise.
85-
86-
The `groups` maps contain all groups for both players.
87-
`distantGroups` contains pairs of indices of groups of the respective player that are at >=2 distance
88-
from each other.
89-
These can be used to check whether a placement is legal, and should be updated after each
90-
placement.
91-
*/
92-
93-
/* Groups per player, groups are mapped to an ID*/
94-
public groups: Map<playerid, Map<number, Set<string>>> = new Map();
95-
/* Pairs of group-IDs {p1: p1groupid, p2: p2-groupid}. When a pair is present in this set,
96-
it indicates that the groups are at sufficient distance (>2) from each other to count as
97-
"free" groups. The ID -1 (on both sides) is reserved for sufficient distance (>1) from the edge.
98-
To properly count as a "free" group, a group must be in this list, distant enough from the edge
99-
(i.e. one of the pairs in this list is (g, -1) or v.v.)
10082
*/
101-
public distantGroups: Set<Map<playerid, number>> = new Set();
10283

10384
/* Expand with a border thickness of n around it */
104-
private expandby(group: Set<string>, n: number): Set<string> {
85+
private expandby(group: Array<string>, n: number): Set<string> {
10586
const newgroup = new Set(group);
10687
for (let i=0; i < n; i++) {
10788
const oldgroup = new Set(newgroup);
@@ -114,160 +95,93 @@ export class StibroGame extends GameBase {
11495
return newgroup;
11596
}
11697

117-
private bothPlayers(player: playerid): [playerid, playerid] {
118-
if(player === 1){
119-
return [1, 2];
120-
} else {
121-
return [2, 1];
122-
}
123-
}
98+
private curredgegroups: Array<Array<string>> | null = null;
99+
private currgrouphalos: Map<number, Set<string>> | null = null;
100+
private othergrouphalos: Map<number, Set<string>> | null = null;
124101

125-
private isEdgeGroup(groupI: number, player: playerid, distantGroups: Set<Map<playerid, number>> = this.distantGroups): boolean {
126-
const [queriedPlayer, otherPlayer] = this.bothPlayers(player);
127-
for(const dist of distantGroups){
128-
if(dist.get(queriedPlayer) === groupI){
129-
if(dist.get(otherPlayer) === -1){
130-
return false;
102+
private freegroupsafter(newCell: string): boolean {
103+
if(this.curredgegroups === null ||
104+
this.currgrouphalos === null ||
105+
this.othergrouphalos === null){
106+
const currgraph = this.getGraph();
107+
const othergraph = this.getGraph();
108+
for (const cell of currgraph.graph.nodes()) {
109+
if(!this.board.has(cell) || this.board.get(cell) == this.otherPlayer()) {
110+
currgraph.graph.dropNode(cell);
111+
}
112+
if(!this.board.has(cell) || this.board.get(cell) == this.currplayer) {
113+
othergraph.graph.dropNode(cell);
131114
}
132115
}
133-
}
134-
return true;
135-
// return !this.distantGroupsSets.get(player)!.get(groupI)!.has(-1);
136-
}
137-
138-
private distantGroupsOf(groupI: number, player: playerid, distantGroups: Set<Map<playerid, number>> = this.distantGroups): Set<number> {
139-
const [queriedPlayer, otherPlayer] = this.bothPlayers(player);
140-
const distantThis: Set<number> = new Set();
141-
for (const distance of distantGroups) {
142-
if (distance.get(queriedPlayer) === groupI) {
143-
distantThis.add(distance.get(otherPlayer)!);
144-
}
145-
}
146-
return distantThis;
147-
}
148116

149-
private touchesOwnGroups(cell: string): boolean {
150-
const cellWithHalo = this.expandby(new Set([cell]), 1);
151-
for(const group of this.groups.get(this.currplayer)!.values()) {
152-
if(this.setIntersection(cellWithHalo, group).size){
117+
const nonEdgeGroup = (group: Array<string>) => {
118+
for (const cell of group) {
119+
if(this.outerRing.has(cell)){
120+
return false;
121+
}
122+
}
153123
return true;
154124
}
155-
}
156-
return false;
157-
}
158-
159-
private newGroupsAndDistantGroups(cell: string): [Map<number, Set<string>>, Set<Map<playerid, number>>]{
160-
/* First add the single new stone as a separate group, including its
161-
distance relations. */
162125

163-
/* Check if it is distant from the edge */
164-
const edge: Set<number> = new Set();
165-
if(!this.outerRing.has(cell)){
166-
edge.add(-1);
167-
}
126+
const currgroups: Array<Array<string>> = []; // non-edge
127+
this.curredgegroups = [];
128+
connectedComponents(currgraph.graph).forEach(group => {
129+
if(nonEdgeGroup(group)){
130+
currgroups.push(group);
131+
} else {
132+
this.curredgegroups!.push(group);
133+
}
134+
});
135+
const othergroups = connectedComponents(othergraph.graph).filter(nonEdgeGroup);
168136

169-
/* Check which opponent groups it is distant from */
170-
const nearbyOpponentGroups: Set<number> = new Set();
171-
const cellWithHalo = this.expandby(new Set([cell]), 2);
172-
for(const [groupkey, group] of this.groups.get(this.otherPlayer())!) {
173-
if(this.setIntersection(cellWithHalo, group).size){
174-
nearbyOpponentGroups.add(groupkey);
175-
}
137+
this.currgrouphalos = new Map(currgroups.map(group => this.expandby(group, 1)).entries());
138+
this.othergrouphalos = new Map(othergroups.map(group => this.expandby(group, 1)).entries());
176139
}
177-
const cellDistantGroups: Set<number> = this.setUnion(
178-
this.setDifference(new Set(this.groups.get(this.otherPlayer())!.keys()),
179-
nearbyOpponentGroups), edge); // Opp. groups + the edge
180-
181-
/* Add the new singleton as a separate group, including distance relations */
182-
const newI: number = Math.max(...this.groups.get(this.currplayer)!.keys(), 0) + 1;
183140

141+
const currgrouphalos = new Map(this.currgrouphalos);
184142

185-
let newGroups: Map<number, Set<string>> = new Map(this.groups.get(this.currplayer));
186-
let newDistantGroups: Set<Map<playerid, number>> = new Set(this.distantGroups);
187-
/* First add the new stone as a separate group, then afterwards check whether
188-
it has to be merged with other groups */
189-
newGroups.set(newI, new Set([cell]));
190-
for(const index of cellDistantGroups){
191-
newDistantGroups.add(new Map([
192-
[this.currplayer, newI],
193-
[this.otherPlayer(), index]
194-
]));
195-
}
143+
const newhalo = this.expandby([newCell], 1);
196144

197-
/* If they touch, merge groups and distances */
198-
const touchedGroups: Set<number> = new Set();
199-
for(const [groupkey, group] of this.groups.get(this.currplayer)!) {
200-
for(const neighbour of this.graph.neighbours(cell)){
201-
if(group.has(neighbour)){
202-
touchedGroups.add(groupkey);
203-
break;
145+
for (const [i, currhalo] of currgrouphalos.entries()) {
146+
if (currhalo.has(newCell)) {
147+
for (const halocell of currhalo) {
148+
newhalo.add(halocell)
204149
}
150+
currgrouphalos.delete(i);
205151
}
206152
}
207153

208-
if(touchedGroups.size){
209-
/* Merge (set union) distant groups of all touching groups */
210-
const distantGroupsToAll: Array<Set<number>> = []; // group indices of other player
211-
for (const touchingI of this.setUnion(touchedGroups, new Set([newI]))){
212-
distantGroupsToAll.push(this.distantGroupsOf(touchingI, this.currplayer, newDistantGroups));
213-
}
214-
215-
// group indices of other player
216-
const mergedDistant: Set<number> = distantGroupsToAll.reduce((a, b) => this.setIntersection(a, b));
217154

218-
/* Merge pieces of all touching groups */
219-
const newGroup = new Set([cell]);
220-
for (const touchedGroupI of touchedGroups) {
221-
for (const groupCell of this.groups.get(this.currplayer)!.get(touchedGroupI)!){
222-
newGroup.add(groupCell);
155+
if(!this.outerRing.has(newCell)){
156+
let newgroupisedgegroup = false;
157+
for (const edgegroupstone of this.curredgegroups.flat()){
158+
if (this.graph.graph.hasEdge(edgegroupstone, newCell)) {
159+
newgroupisedgegroup = true;
160+
break;
223161
}
224162
}
225-
226-
const obsoleteGroups = this.setUnion(touchedGroups, new Set([newI])); // group indices of curr player
227-
228-
/* Remove obsolete distance relations */
229-
newDistantGroups = new Set([...newDistantGroups].filter((dist) =>
230-
!obsoleteGroups.has(dist.get(this.currplayer)!)))
231-
232-
/* Remove obsolete groups */
233-
newGroups = new Map([...newGroups.entries()]
234-
.filter((item) => !obsoleteGroups.has(item[0])));
235-
236-
/* Add new distance relations */
237-
for (const otherI of mergedDistant) {
238-
const newDist: Map<playerid, number> = new Map([
239-
[this.currplayer, newI],
240-
[this.otherPlayer(), otherI]
241-
]);
242-
newDistantGroups.add(newDist);
163+
if (!newgroupisedgegroup){
164+
currgrouphalos.set(-1, newhalo);
243165
}
244-
245-
/* Add new merged group */
246-
newGroups.set(newI, newGroup);
247-
248-
}
249-
return [newGroups, newDistantGroups];
250-
}
251-
252-
private freegroupsafter(cell: string): boolean {
253-
if(this.groups.get(this.currplayer)!.size && !this.touchesOwnGroups(cell)){
254-
/* fast pre-check: it doesn't touch any of its own groups */
255-
return true;
256166
}
257167

258-
const [newGroups, newDistantGroups] = this.newGroupsAndDistantGroups(cell);
259-
260-
for(const thisI of newGroups.keys()) {
261-
if(!this.isEdgeGroup(thisI, this.currplayer, newDistantGroups)){
262-
/* It is free if a group at a distance does not touch the edge */
263-
for(const otherGroupI of this.distantGroupsOf(thisI, this.currplayer, newDistantGroups)){
264-
if(!this.isEdgeGroup(otherGroupI, this.otherPlayer(), newDistantGroups)){
265-
return true;
168+
/* If there is (at least) a single group whose halo has no overlap with a halo of (at least)
169+
a single opponent group, the placement restriction is satisfied. */
170+
for (const currhalo of currgrouphalos.values()) {
171+
for (const otherhalo of this.othergrouphalos.values()) {
172+
let skiphalo = false;
173+
for (const currcell of currhalo) {
174+
if (otherhalo.has(currcell)) {
175+
skiphalo = true;
176+
break;
266177
}
267178
}
179+
if (!skiphalo) {
180+
/* No overlap was found between currhalo and otherhalo, satisfying the restriction */
181+
return true;
182+
}
268183
}
269184
}
270-
271185
return false;
272186
}
273187

@@ -327,11 +241,6 @@ export class StibroGame extends GameBase {
327241
currplayer: 1,
328242
board: new Map<string, cellcontent>(),
329243
winningLoop: [],
330-
groups: new Map([
331-
[1, new Map<number, Set<string>>()],
332-
[2, new Map<number, Set<string>>()],
333-
]),
334-
distantGroups: new Set()
335244
};
336245
this.stack = [fresh];
337246

@@ -364,8 +273,6 @@ export class StibroGame extends GameBase {
364273
this.board = new Map(state.board);
365274
this.lastmove = state.lastmove;
366275
this.winningLoop = [...state.winningLoop];
367-
this.groups = state.groups;
368-
this.distantGroups = state.distantGroups;
369276

370277
return this;
371278
}
@@ -376,13 +283,14 @@ export class StibroGame extends GameBase {
376283
}
377284

378285
// First placement
379-
if (this.groups.get(this.otherPlayer())!.size === 0) {
286+
if (this.board.size === 0) {
380287
return !this.outerRing.has(cell);
381288
}
382289

383290
if (this.board.has(cell)) { // occupied
384291
return false;
385292
}
293+
386294
return this.freegroupsafter(cell);
387295
}
388296

@@ -392,7 +300,7 @@ export class StibroGame extends GameBase {
392300
}
393301

394302
// First placement
395-
if (this.groups.get(this.otherPlayer())!.size === 0) {
303+
if (this.board.size === 0) {
396304
return [!this.outerRing.has(cell), "firstplacement"];
397305
}
398306

@@ -507,16 +415,19 @@ export class StibroGame extends GameBase {
507415
throw new UserFacingError("VALIDATION_GENERAL", result.message)
508416
}
509417
}
418+
419+
/* invalidate cache */
420+
this.curredgegroups = null;
421+
this.currgrouphalos = null;
422+
this.othergrouphalos = null;
423+
510424
this.results = [];
511425

512426
const cell = m;
513427

514428
const piece = this.currplayer;
515429

516430
this.board.set(cell, piece);
517-
const [newGroups, newDistantGroups] = this.newGroupsAndDistantGroups(cell);
518-
this.groups.set(this.currplayer, newGroups);
519-
this.distantGroups = newDistantGroups;
520431

521432
this.results.push({type: "place", where: cell});
522433

@@ -704,8 +615,6 @@ export class StibroGame extends GameBase {
704615
lastmove: this.lastmove,
705616
board: new Map(this.board),
706617
winningLoop: [...this.winningLoop],
707-
groups: this.groups,
708-
distantGroups: this.distantGroups
709618
};
710619
return state;
711620
}
@@ -767,16 +676,4 @@ export class StibroGame extends GameBase {
767676
public clone(): StibroGame {
768677
return new StibroGame(this.serialize());
769678
}
770-
771-
private setUnion<Type>(a: Set<Type>, b: Set<Type>): Set<Type> {
772-
return new Set([...a, ...b]);
773-
}
774-
775-
private setIntersection<Type>(a: Set<Type>, b: Set<Type>): Set<Type> {
776-
return new Set([...a].filter(x => b.has(x)));
777-
}
778-
779-
private setDifference<Type>(a: Set<Type>, b: Set<Type>): Set<Type> {
780-
return new Set([...a].filter(x => !b.has(x)));
781-
}
782679
};

0 commit comments

Comments
 (0)