@@ -6,12 +6,13 @@ import { HexTriGraph, reviver, UserFacingError } from "../common";
66import type { HexDir } from "../common/graphs/hextri" ;
77import i18next from "i18next" ;
88
9- export type playerid = 1 | 2 ;
9+ export type playerid = 1 | 2 | 3 ;
1010
1111export interface IMoveState extends IIndividualState {
1212 currplayer : playerid ;
1313 board : Map < string , playerid > ;
1414 lastmove ?: string ;
15+ eliminated ?: playerid ;
1516} ;
1617
1718export interface IYavalathState extends IAPGameState {
@@ -23,7 +24,7 @@ export class YavalathGame extends GameBase {
2324 public static readonly gameinfo : APGamesInformation = {
2425 name : "Yavalath" ,
2526 uid : "yavalath" ,
26- playercounts : [ 2 ] ,
27+ playercounts : [ 2 , 3 ] ,
2728 version : "20250112" ,
2829 dateAdded : "2025-01-12" ,
2930 // i18next.t("apgames:descriptions.yavalath")
@@ -48,10 +49,12 @@ export class YavalathGame extends GameBase {
4849 public variants : string [ ] = [ ] ;
4950 public stack ! : Array < IMoveState > ;
5051 public results : Array < APMoveResult > = [ ] ;
52+ public eliminated ?: playerid ;
5153
52- constructor ( state ? : IYavalathState | string ) {
54+ constructor ( state : IYavalathState | string | number ) {
5355 super ( ) ;
54- if ( state === undefined ) {
56+ if ( typeof state === "number" ) {
57+ this . numplayers = state ;
5558 const board = new Map < string , playerid > ( ) ;
5659 const fresh : IMoveState = {
5760 _version : YavalathGame . gameinfo . version ,
@@ -68,6 +71,7 @@ export class YavalathGame extends GameBase {
6871 if ( state . game !== YavalathGame . gameinfo . uid ) {
6972 throw new Error ( `The Yavalath engine cannot process a game of '${ state . game } '.` ) ;
7073 }
74+ this . numplayers = state . numplayers ;
7175 this . gameover = state . gameover ;
7276 this . winner = [ ...state . winner ] ;
7377 this . variants = state . variants ;
@@ -88,12 +92,17 @@ export class YavalathGame extends GameBase {
8892 this . currplayer = state . currplayer ;
8993 this . board = new Map ( state . board ) ;
9094 this . lastmove = state . lastmove ;
95+ this . eliminated = state . eliminated ;
9196 return this ;
9297 }
9398
9499 public moves ( ) : string [ ] {
95100 if ( this . gameover ) { return [ ] ; }
96101
102+ if ( this . eliminated !== undefined && this . eliminated === this . currplayer ) {
103+ return [ "pass" ] ;
104+ }
105+
97106 const g = new HexTriGraph ( 5 , 9 ) ;
98107 const moves = g . graph . nodes ( ) . filter ( n => ! this . board . has ( n ) ) ;
99108 return moves . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
@@ -139,6 +148,20 @@ export class YavalathGame extends GameBase {
139148 return result ;
140149 }
141150
151+ // pass only valid if you're eliminated
152+ if ( m === "pass" ) {
153+ if ( this . eliminated !== undefined && this . eliminated === this . currplayer ) {
154+ result . valid = true ;
155+ result . complete = 1 ;
156+ result . message = i18next . t ( "apgames:validation._general.VALID_MOVE" ) ;
157+ return result ;
158+ } else {
159+ result . valid = false ;
160+ result . message = i18next . t ( "apgames:validation.yavalath.BAD_PASS" , { where : m } ) ;
161+ return result ;
162+ }
163+ }
164+
142165 // cell must exist
143166 if ( ! g . graph . nodes ( ) . includes ( m ) ) {
144167 result . valid = false ;
@@ -177,15 +200,26 @@ export class YavalathGame extends GameBase {
177200 }
178201
179202 this . results = [ ] ;
180- this . board . set ( m , this . currplayer ) ;
181- this . results . push ( { type : "place" , where : m } ) ;
203+
204+ if ( m === "pass" ) {
205+ this . results . push ( { type : "pass" } ) ;
206+ } else {
207+ this . board . set ( m , this . currplayer ) ;
208+ this . results . push ( { type : "place" , where : m } ) ;
209+ }
182210
183211 // update currplayer
184212 this . lastmove = m ;
185213 let newplayer = ( this . currplayer as number ) + 1 ;
186214 if ( newplayer > this . numplayers ) {
187215 newplayer = 1 ;
188216 }
217+ while ( this . eliminated !== undefined && this . eliminated === newplayer ) {
218+ newplayer = ( newplayer as number ) + 1 ;
219+ if ( newplayer > this . numplayers ) {
220+ newplayer = 1 ;
221+ }
222+ }
189223 this . currplayer = newplayer as playerid ;
190224
191225 this . checkEOG ( ) ;
@@ -227,25 +261,52 @@ export class YavalathGame extends GameBase {
227261 }
228262
229263 protected checkEOG ( ) : YavalathGame {
230- const prevPlayer = this . currplayer === 1 ? 2 : 1 ;
231- const hasFour = this . checkLines ( 4 , prevPlayer ) ;
232- const hasThree = this . checkLines ( 3 , prevPlayer ) ;
233- const g = new HexTriGraph ( 5 , 9 ) ;
264+ let prevPlayer : playerid ;
265+ if ( this . numplayers === 2 ) {
266+ prevPlayer = this . currplayer === 1 ? 2 : 1 ;
267+ } else if ( this . eliminated === undefined ) {
268+ prevPlayer = this . currplayer === 1 ? 3 : this . currplayer === 3 ? 2 : 1 ;
269+ } else {
270+ const remaining = ( [ 1 , 2 , 3 ] as playerid [ ] ) . filter ( p => p !== this . eliminated ) ;
271+ prevPlayer = this . currplayer === remaining [ 0 ] ? remaining [ 1 ] : remaining [ 0 ] ;
272+ }
234273
235- // if previous player has four, they win
274+ // regardless of number of players, four in a row is a win
275+ const hasFour = this . checkLines ( 4 , prevPlayer ) ;
236276 if ( hasFour ) {
237277 this . gameover = true ;
238278 this . winner = [ prevPlayer ] ;
239279 }
240- // if previous player has three, they lose
241- else if ( hasThree ) {
242- this . gameover = true ;
243- this . winner = [ this . currplayer ] ;
280+
281+ if ( ! this . gameover ) {
282+ // if they have a three, then results vary
283+ const hasThree = this . checkLines ( 3 , prevPlayer ) ;
284+ if ( hasThree ) {
285+ if ( this . numplayers === 2 ) {
286+ this . gameover = true ;
287+ this . winner = [ this . currplayer ] ;
288+ } else if ( this . eliminated === undefined ) {
289+ this . eliminated = prevPlayer ;
290+ } else {
291+ this . gameover = true ;
292+ this . winner = ( [ 1 , 2 , 3 ] as playerid [ ] ) . filter ( p => p !== this . eliminated && p !== prevPlayer ) ;
293+ }
294+ }
244295 }
245- // if board full, draw
246- else if ( g . graph . nodes ( ) . filter ( n => ! this . board . has ( n ) ) . length === 0 ) {
247- this . gameover = true ;
248- this . winner = [ 1 , 2 ] ;
296+
297+ // regardless of number of players, a full board is a draw
298+ if ( ! this . gameover ) {
299+ const g = new HexTriGraph ( 5 , 9 ) ;
300+ if ( g . graph . nodes ( ) . filter ( n => ! this . board . has ( n ) ) . length === 0 ) {
301+ this . gameover = true ;
302+ if ( this . numplayers === 2 ) {
303+ this . winner = [ 1 , 2 ] ;
304+ } else if ( this . eliminated === undefined ) {
305+ this . winner = [ 1 , 2 , 3 ] ;
306+ } else {
307+ this . winner = ( [ 1 , 2 , 3 ] as playerid [ ] ) . filter ( p => p !== this . eliminated ) ;
308+ }
309+ }
249310 }
250311
251312 if ( this . gameover ) {
@@ -276,6 +337,7 @@ export class YavalathGame extends GameBase {
276337 currplayer : this . currplayer ,
277338 lastmove : this . lastmove ,
278339 board : new Map ( this . board ) ,
340+ eliminated : this . eliminated ,
279341 } ;
280342 }
281343
@@ -292,7 +354,8 @@ export class YavalathGame extends GameBase {
292354 if ( ! this . board . has ( cell ) ) {
293355 pieces . push ( "-" ) ;
294356 } else {
295- pieces . push ( this . board . get ( cell ) ! === 1 ? "A" : "B" ) ;
357+ const contents = this . board . get ( cell ) ! ;
358+ pieces . push ( contents === 1 ? "A" : contents === 2 ? "B" : "C" ) ;
296359 }
297360 }
298361 pstr += pieces . join ( "" ) ;
@@ -313,6 +376,10 @@ export class YavalathGame extends GameBase {
313376 B : {
314377 name : "piece" ,
315378 colour : 2
379+ } ,
380+ C : {
381+ name : "piece" ,
382+ colour : 3
316383 }
317384 } ,
318385 pieces : pstr
0 commit comments