@@ -93,7 +93,7 @@ export class RincalaGame extends GameBase {
9393 } ,
9494 ] ,
9595 categories : [ "goal>score>eog" , "mechanic>move>sow" , "mechanic>capture" , "board>shape>circle" , "board>connect>linear" , "components>simple>4c" ] ,
96- flags : [ "automove " , "scores" , "random-start" , "experimental" ]
96+ flags : [ "no-moves" , "custom-randomization ", "scores" , "random-start" , "experimental" ]
9797 } ;
9898
9999 public static value ( pc : Colour ) : number {
@@ -168,52 +168,52 @@ export class RincalaGame extends GameBase {
168168 return this ;
169169 }
170170
171- public moves ( ) : string [ ] {
172- if ( this . gameover ) { return [ ] ; }
173-
174- const moves : string [ ] = [ ] ;
175-
176- // for first move of the game, just do gatherMoves and leave it at that
177- if ( this . stack . length === 1 ) {
178- moves . push ( ...this . gatherMoves ( ) . map ( ( { move} ) => move ) ) ;
179- }
180- // otherwise, recurse
181- else {
182- this . recurseMoves ( moves , null ) ;
183- }
184-
185- return [ ...moves ] . sort ( ( a , b ) => {
186- if ( a . length === b . length ) {
187- return a . localeCompare ( b ) ;
188- } else {
189- return a . length - b . length ;
190- }
191- } ) ;
192- }
193-
194- public recurseMoves ( moves : string [ ] , working : string [ ] | null ) : void {
195- // null means very first time
196- if ( working === null ) {
197- const results = this . gatherMoves ( ) ;
198- // store all terminal moves
199- moves . push ( ...results . filter ( ( { terminal} ) => terminal ) . map ( ( { move} ) => move ) ) ;
200- // recurse with any nonterminal moves
201- this . recurseMoves ( moves , results . filter ( ( { terminal} ) => ! terminal ) . map ( ( { move} ) => move ) ) ;
202- }
203- // otherwise we have some starting moves
204- else {
205- for ( const mv of working ) {
206- const cloned = this . clone ( ) ;
207- cloned . move ( mv , { partial : true , trusted : true } ) ;
208- const results = cloned . gatherMoves ( ) ;
209- // store all terminal moves
210- moves . push ( ...results . filter ( ( { terminal} ) => terminal ) . map ( ( { move} ) => move ) . map ( m => `${ mv } ,${ m } ` ) ) ;
211- // recurse with any nonterminal moves
212- const nonterminal = results . filter ( ( { terminal} ) => ! terminal ) . map ( ( { move} ) => move ) . map ( m => `${ mv } ,${ m } ` ) ;
213- this . recurseMoves ( moves , nonterminal ) ;
214- }
215- }
216- }
171+ // public moves(): string[] {
172+ // if (this.gameover) { return []; }
173+
174+ // const moves: string[] = [];
175+
176+ // // for first move of the game, just do gatherMoves and leave it at that
177+ // if (this.stack.length === 1) {
178+ // moves.push(...this.gatherMoves().map(({move}) => move));
179+ // }
180+ // // otherwise, recurse
181+ // else {
182+ // this.recurseMoves(moves, null);
183+ // }
184+
185+ // return [...moves].sort((a,b) => {
186+ // if (a.length === b.length) {
187+ // return a.localeCompare(b);
188+ // } else {
189+ // return a.length - b.length;
190+ // }
191+ // });
192+ // }
193+
194+ // public recurseMoves(moves: string[], working: string[]|null): void {
195+ // // null means very first time
196+ // if (working === null) {
197+ // const results = this.gatherMoves();
198+ // // store all terminal moves
199+ // moves.push(...results.filter(({terminal}) => terminal).map(({move}) => move));
200+ // // recurse with any nonterminal moves
201+ // this.recurseMoves(moves, results.filter(({terminal}) => !terminal).map(({move}) => move));
202+ // }
203+ // // otherwise we have some starting moves
204+ // else {
205+ // for (const mv of working) {
206+ // const cloned = this.clone();
207+ // cloned.move(mv, {partial: true, trusted: true});
208+ // const results = cloned.gatherMoves();
209+ // // store all terminal moves
210+ // moves.push(...results.filter(({terminal}) => terminal).map(({move}) => move).map(m => `${mv},${m}`));
211+ // // recurse with any nonterminal moves
212+ // const nonterminal = results.filter(({terminal}) => !terminal).map(({move}) => move).map(m => `${mv},${m}`);
213+ // this.recurseMoves(moves, nonterminal);
214+ // }
215+ // }
216+ // }
217217
218218 // gets a list of all single legal moves from a given position,
219219 // including whether the move was terminal (capture or empty hollow)
@@ -289,8 +289,28 @@ export class RincalaGame extends GameBase {
289289 }
290290
291291 public randomMove ( ) : string {
292- const moves = this . moves ( ) ;
293- return moves [ Math . floor ( Math . random ( ) * moves . length ) ] ;
292+ const steps : string [ ] = [ ] ;
293+ let step : { move : string ; terminal : boolean } ;
294+ let cloned = this . clone ( ) ;
295+ const onlyOne = this . stack . length === 1 ;
296+ do {
297+ const moves = cloned . gatherMoves ( ) ;
298+ if ( moves . length > 0 ) {
299+ step = moves [ Math . floor ( Math . random ( ) * moves . length ) ] ;
300+ steps . push ( step . move ) ;
301+ cloned = this . clone ( ) ;
302+ cloned . move ( steps . join ( "," ) , { partial : true , trusted : true } ) ;
303+ if ( onlyOne ) break ;
304+ } else {
305+ return "pass" ;
306+ }
307+ } while ( ! step . terminal ) ;
308+ const combined = steps . join ( "," ) ;
309+ const result = this . validateMove ( combined ) ;
310+ if ( ! result . valid || result . complete !== 1 ) {
311+ throw new Error ( `The move ${ combined } was generated but is not valid.` ) ;
312+ }
313+ return steps . join ( "," ) ;
294314 }
295315
296316 public getDirection ( first : number , second : number ) : Direction | undefined {
@@ -359,30 +379,59 @@ export class RincalaGame extends GameBase {
359379 return result ;
360380 }
361381
362- const allmoves = this . moves ( ) ;
363382 const steps = m . split ( "," ) ;
364- if ( allmoves . filter ( mv => mv . startsWith ( m ) ) . length > 0 ) {
365- // if the exact move is found, we're done
366- if ( allmoves . includes ( m ) ) {
367- result . valid = true ;
368- result . complete = 1 ;
369- result . message = i18next . t ( "apgames:validation._general.VALID_MOVE" ) ;
383+ const last = steps . pop ( ) ! ;
384+ let cloned = this . clone ( ) ;
385+ // validate each step
386+ for ( let i = 0 ; i < steps . length ; i ++ ) {
387+ cloned = this . clone ( ) ;
388+ const moves = cloned . gatherMoves ( ) ;
389+ const found = moves . find ( ( { move} ) => move === steps [ i ] ) ;
390+ if ( found === undefined || found . terminal ) {
391+ result . valid = false ;
392+ result . message = i18next . t ( "apgames:validation._general.INVALID_MOVE" , { move : steps . slice ( 0 , i + 1 ) . join ( "," ) } ) ;
370393 return result ;
371394 }
372- // if the last step is incomplete, then partial
373- else if ( steps [ steps . length - 1 ] . length === 1 ) {
374- result . valid = true ;
375- result . complete = - 1 ;
376- result . canrender = true ;
377- result . message = i18next . t ( "apgames:validation.rincala.PARTIAL" ) ;
378- return result ;
395+ cloned . move ( steps . slice ( 0 , i + 1 ) . join ( "," ) , { partial : true , trusted : true } ) ;
396+ }
397+ // validate very last step
398+ const moves = cloned . gatherMoves ( ) ;
399+ if ( moves . filter ( ( { move} ) => move . startsWith ( last ) ) . length > 0 ) {
400+ const found = moves . find ( ( { move} ) => move === last ) ;
401+ // exact match
402+ if ( found !== undefined ) {
403+ // if first move of the game, only one step is allowed, so ignore terminal
404+ if ( this . stack . length === 1 ) {
405+ result . valid = true ;
406+ result . complete = 1 ;
407+ result . message = i18next . t ( "apgames:validation._general.VALID_MOVE" ) ;
408+ return result ;
409+ }
410+ // every other time
411+ else {
412+ // terminal
413+ if ( found . terminal ) {
414+ result . valid = true ;
415+ result . complete = 1 ;
416+ result . message = i18next . t ( "apgames:validation._general.VALID_MOVE" ) ;
417+ return result ;
418+ }
419+ // must continue
420+ else {
421+ result . valid = true ;
422+ result . complete = - 1 ;
423+ result . canrender = true ;
424+ result . message = i18next . t ( "apgames:validation.rincala.CONTINUE" ) ;
425+ return result ;
426+ }
427+ }
379428 }
380- // otherwise, the last step was not terminal so you just need to keep going
429+ // if the last step is incomplete, then partial
381430 else {
382431 result . valid = true ;
383432 result . complete = - 1 ;
384433 result . canrender = true ;
385- result . message = i18next . t ( "apgames:validation.rincala.CONTINUE " ) ;
434+ result . message = i18next . t ( "apgames:validation.rincala.PARTIAL " ) ;
386435 return result ;
387436 }
388437 } else {
@@ -394,8 +443,6 @@ export class RincalaGame extends GameBase {
394443 }
395444 }
396445
397- // The partial flag enabled dynamic connection checking.
398- // It leaves the object in an invalid state, so only use it on cloned objects, or call `load()` before submitting again.
399446 public move ( m : string , { partial = false , trusted = false } = { } ) : RincalaGame {
400447 if ( this . gameover ) {
401448 throw new UserFacingError ( "MOVES_GAMEOVER" , i18next . t ( "apgames:MOVES_GAMEOVER" ) ) ;
@@ -409,9 +456,12 @@ export class RincalaGame extends GameBase {
409456 if ( ! result . valid ) {
410457 throw new UserFacingError ( "VALIDATION_GENERAL" , result . message )
411458 }
412- if ( ! partial && ! this . moves ( ) . includes ( m ) ) {
413- throw new UserFacingError ( "VALIDATION_FAILSAFE " , i18next . t ( "apgames:validation._general.FAILSAFE" , { move : m } ) )
459+ if ( ! partial && result . complete !== 1 ) {
460+ throw new UserFacingError ( "VALIDATION_GENERAL " , result . message )
414461 }
462+ // if (!partial && !this.moves().includes(m)) {
463+ // throw new UserFacingError("VALIDATION_FAILSAFE", i18next.t("apgames:validation._general.FAILSAFE", {move: m}))
464+ // }
415465 }
416466
417467 this . results = [ ] ;
@@ -423,13 +473,18 @@ export class RincalaGame extends GameBase {
423473
424474 const steps = m . split ( "," ) . filter ( Boolean ) ;
425475 for ( const step of steps ) {
426- // skip incomplete steps
427- if ( step . length < 2 ) break ;
476+ // skip incomplete steps when partial
477+ if ( partial && step . length < 2 ) {
478+ break ;
479+ }
480+ // otherwise throw
481+ else if ( step . length < 2 ) {
482+ throw new Error ( "Incomplete move somehow made it through." ) ;
483+ }
428484 const results : APMoveResult [ ] = [ ] ;
429485 const [ startLbl , dirstr ] = step . split ( "" ) ;
430486 const dir : Direction = dirstr === ">" ? "CW" : "CCW" ;
431487 const start = RincalaGame . lbl2col ( startLbl ) ;
432- // console.log(JSON.stringify({startLbl, dirstr, dir, start}));
433488 const pits = this . mv2pits ( start , dir ) ;
434489 const stack = [ ...this . board [ start ] ] ;
435490 this . board [ start ] = [ ] ;
@@ -484,7 +539,7 @@ export class RincalaGame extends GameBase {
484539 // game ends if there is only 4 pieces left on the board
485540 // (by definition, this would be one of each colour)
486541 // or if there are no moves available
487- if ( this . board . flat ( ) . length === 4 || this . moves ( ) . length === 0 ) {
542+ if ( this . board . flat ( ) . length === 4 || this . gatherMoves ( ) . length === 0 ) {
488543 this . gameover = true ;
489544 const score1 = this . getPlayerScore ( 1 ) ;
490545 const score2 = this . getPlayerScore ( 2 ) ;
@@ -691,6 +746,24 @@ export class RincalaGame extends GameBase {
691746 return this . hands [ player - 1 ] . map ( pc => RincalaGame . value ( pc ) ) . reduce ( ( a , b ) => a + b , 0 ) ;
692747 }
693748
749+ public chat ( node : string [ ] , player : string , results : APMoveResult [ ] , r : APMoveResult ) : boolean {
750+ if ( r . type === "_group" ) {
751+ let resolved = true ;
752+ for ( const nested of r . results ) {
753+ if ( nested . type === "sow" ) {
754+ node . push ( i18next . t ( "apresults:SOW.rincala" , { player, count : nested . pieces ! . length , pieces : nested . pieces ! . join ( "," ) , from : nested . from , to : nested . to ! . join ( "," ) } ) ) ;
755+ } else if ( nested . type === "capture" ) {
756+ node . push ( i18next . t ( "apresults:CAPTURE.complete" , { player, where : nested . where , what : nested . what } ) ) ;
757+ } else {
758+ resolved = false ;
759+ break ;
760+ }
761+ }
762+ return resolved ;
763+ }
764+ return false ;
765+ }
766+
694767 public sameMove ( move1 : string , move2 : string ) : boolean {
695768 // if either move contains an open parenthesis (showing captures),
696769 // only compare everything up to that parenthesis.
0 commit comments