@@ -4,6 +4,7 @@ import { APRenderRep, RowCol } from "@abstractplay/renderer/src/schemas/schema";
44import { APMoveResult } from "../schemas/moveresults" ;
55import { HexTriGraph , reviver , UserFacingError , StackSet } from "../common" ;
66import { bfsFromNode , dfsFromNode } from 'graphology-traversal' ;
7+ import { connectedComponents } from 'graphology-components' ;
78import 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
2221export 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