@@ -1,9 +1,11 @@
import { describe , it , expect , beforeAll } from 'vitest' ;
import { describe , it , expect , beforeAll } from 'vitest' ;
import { evaluateLocal } from './eval' ;
import { evaluateLocal } from './eval' ;
import { playedWordsFromHistory } from './norepeat' ;
import { seedMockAlphabets } from '../mock/alphabet' ;
import { seedMockAlphabets } from '../mock/alphabet' ;
import { emptyBoard , type Board } from '../board' ;
import { emptyBoard , type Board } from '../board' ;
import type { Dict } from './validate' ;
import type { Dict } from './validate' ;
import type { PlacedTile } from '../client' ;
import type { PlacedTile } from '../client' ;
import type { MoveRecord } from '../model' ;
// The no-repeat-words rule as the game screen sees it: through evaluateLocal, the on-device move
// The no-repeat-words rule as the game screen sees it: through evaluateLocal, the on-device move
// preview, which decides the rule before a play ever reaches the server. The dictionary is stubbed
// preview, which decides the rule before a play ever reaches the server. The dictionary is stubbed
@@ -21,6 +23,31 @@ function tile(row: number, col: number, letter: string): PlacedTile {
return { row , col , letter , blank : false } ;
return { row , col , letter , blank : false } ;
}
}
// playRecord is one history entry as the client holds it — note the UPPER-CASE words, which is how
// the codec decodes them for display (lib/codec decodeMove). Building the played set from records
// in this shape is what the game screen actually does, so these tests go through it rather than
// hand-writing the set; comparing the codec's case against the evaluator's would otherwise silently
// never match and the rule would never fire.
function playRecord ( . . . words : string [ ] ) : MoveRecord {
return {
player : 0 ,
action : 'play' ,
dir : 'H' ,
mainRow : 0 ,
mainCol : 0 ,
tiles : [ ] ,
words : words.map ( ( w ) = > w . toUpperCase ( ) ) ,
count : 0 ,
score : 0 ,
total : 0 ,
} ;
}
// passRecord is a non-play entry, which contributes no words.
function passRecord ( ) : MoveRecord {
return { player : 0 , action : 'pass' , dir : '' , mainRow : 0 , mainCol : 0 , tiles : [ ] , words : [ ] , count : 0 , score : 0 , total : 0 } ;
}
beforeAll ( ( ) = > seedMockAlphabets ( ) ) ;
beforeAll ( ( ) = > seedMockAlphabets ( ) ) ;
describe ( 'evaluateLocal under the no-repeat-words rule' , ( ) = > {
describe ( 'evaluateLocal under the no-repeat-words rule' , ( ) = > {
@@ -44,7 +71,12 @@ describe('evaluateLocal under the no-repeat-words rule', () => {
}
}
// Б above the standing Р and А below it spell БРА downwards.
// Б above the standing Р and А below it spell БРА downwards.
const bra : PlacedTile [ ] = [ tile ( 10 , 3 , 'б ' ) , tile ( 12 , 3 , 'а ' ) ] ;
const bra : PlacedTile [ ] = [ tile ( 10 , 3 , 'б ' ) , tile ( 12 , 3 , 'а ' ) ] ;
const played = new Set ( [ 'травля' , 'марал' , 'кот' , 'наем' , 'бра' , 'пойка' , 'овен' , 'джеб' , 'путы' , 'мысик' , 'суши' , 'лист' , 'плуг' , 'принт' ] ) ;
const history : MoveRecord [ ] = [
playRecord ( 'травля' ) , playRecord ( 'марал' ) , playRecord ( 'кот' ) , playRecord ( 'наем' ) , playRecord ( 'бра' ) ,
playRecord ( 'пойка' ) , playRecord ( 'овен' ) , playRecord ( 'джеб' ) , playRecord ( 'путы' ) , playRecord ( 'мысик' ) ,
playRecord ( 'суши' ) , playRecord ( 'лист' ) , playRecord ( 'плуг' ) , playRecord ( 'принт' ) , passRecord ( ) ,
] ;
const played = playedWordsFromHistory ( history ) ;
it ( 'scores the play when the game does not take the rule' , ( ) = > {
it ( 'scores the play when the game does not take the rule' , ( ) = > {
// The control: the same play in a game without the rule is a perfectly ordinary six-pointer.
// The control: the same play in a game without the rule is a perfectly ordinary six-pointer.
@@ -64,7 +96,7 @@ describe('evaluateLocal under the no-repeat-words rule', () => {
} ) ;
} ) ;
it ( 'leaves the play alone when the rule is in force but the word is new' , ( ) = > {
it ( 'leaves the play alone when the rule is in force but the word is new' , ( ) = > {
const res = evaluateLocal ( anyWord , 'erudit_ru' , contourBoard ( ) , bra , false , new Set ( [ 'кот' ] ) ) ;
const res = evaluateLocal ( anyWord , 'erudit_ru' , contourBoard ( ) , bra , false , playedWordsFromHistory ( [ playRecord ( 'кот' ) ] ) ) ;
expect ( res . legal ) . toBe ( true ) ;
expect ( res . legal ) . toBe ( true ) ;
expect ( res . score ) . toBe ( 6 ) ;
expect ( res . score ) . toBe ( 6 ) ;
} ) ;
} ) ;
@@ -89,7 +121,7 @@ describe('evaluateLocal under the rule with several words per turn', () => {
expect ( free . words ) . toEqual ( [ 'рот' , 'кот' ] ) ;
expect ( free . words ) . toEqual ( [ 'рот' , 'кот' ] ) ;
expect ( free . score ) . toBe ( 10 ) ; // РОТ plus the КОТ cross-word
expect ( free . score ) . toBe ( 10 ) ; // РОТ plus the КОТ cross-word
const restricted = evaluateLocal ( anyWord , 'erudit_ru' , crossBoard ( ) , rot , true , new Set ( [ 'кот' ] ) ) ;
const restricted = evaluateLocal ( anyWord , 'erudit_ru' , crossBoard ( ) , rot , true , playedWordsFromHistory ( [ playRecord ( 'кот' ) ] ) ) ;
expect ( restricted . legal ) . toBe ( true ) ;
expect ( restricted . legal ) . toBe ( true ) ;
expect ( restricted . repeatedWord ) . toBeUndefined ( ) ;
expect ( restricted . repeatedWord ) . toBeUndefined ( ) ;
// The cross-word is still formed and still reported — it just earns nothing.
// The cross-word is still formed and still reported — it just earns nothing.
@@ -98,7 +130,7 @@ describe('evaluateLocal under the rule with several words per turn', () => {
} ) ;
} ) ;
it ( 'rejects the play when the main word repeats, whatever its cross-words do' , ( ) = > {
it ( 'rejects the play when the main word repeats, whatever its cross-words do' , ( ) = > {
const res = evaluateLocal ( anyWord , 'erudit_ru' , crossBoard ( ) , rot , true , new Set ( [ 'рот' , 'кот' ] ) ) ;
const res = evaluateLocal ( anyWord , 'erudit_ru' , crossBoard ( ) , rot , true , playedWordsFromHistory ( [ playRecord ( 'рот' , 'кот' ) ] ) ) ;
expect ( res . legal ) . toBe ( false ) ;
expect ( res . legal ) . toBe ( false ) ;
expect ( res . repeatedWord ) . toBe ( 'рот' ) ;
expect ( res . repeatedWord ) . toBe ( 'рот' ) ;
} ) ;
} ) ;