@@ -1,6 +1,11 @@
package engine
import "testing"
import (
"errors"
"testing"
"gitea.iliadenisov.ru/developer/scrabble-solver/scrabble"
)
// TestSingleWordRuleWiring confirms Options.MultipleWordsPerTurn reaches the solver. The
// single-word game ignores perpendicular cross-words, so move generation from a shared
@@ -46,3 +51,115 @@ func TestSingleWordRuleWiring(t *testing.T) {
t . Errorf ( "single-word generation produced %d moves, want >= standard %d" , singleMoves , stdMoves )
}
}
// setupSingleWordKran builds an Erudit position that reproduces the test-contour
// bug. It replaces the bag-dealt rack with к/а /н and places the existing Р the play
// bridges plus perpendicular neighbours (г , е , н) so that each of the three new
// tiles of the vertical КРАН forms an *invalid* cross-word (гк, еа, нн). The
// multipleWords argument selects the rule. It returns the game and the decoded КРАН
// placement (the three new tiles К , А , Н around the existing Р ).
func setupSingleWordKran ( t * testing . T , multipleWords bool ) ( * Game , [ ] TileRecord ) {
t . Helper ( )
g , err := New ( testReg , Options {
Variant : VariantErudit ,
Version : testVersion ,
Players : 2 ,
Seed : 1 ,
MultipleWordsPerTurn : multipleWords ,
} )
if err != nil {
t . Fatalf ( "new erudit game: %v" , err )
}
idx := func ( s string ) byte {
i , err := g . rules . Alphabet . Index ( s )
if err != nil {
t . Fatalf ( "index %q: %v" , s , err )
}
return i
}
scrabble . Apply ( g . board , scrabble . Move { Tiles : [ ] scrabble . Placement {
{ Row : 5 , Col : 8 , Letter : idx ( "р " ) } , // the existing tile the play bridges
{ Row : 4 , Col : 7 , Letter : idx ( "г " ) } , // left of К (4,8): cross-word "гк"
{ Row : 6 , Col : 7 , Letter : idx ( "е " ) } , // left of А (6,8): cross-word "еа"
{ Row : 7 , Col : 7 , Letter : idx ( "н" ) } , // left of Н (7,8): cross-word "нн"
} } )
g . hands [ 0 ] = [ ] byte { idx ( "к" ) , idx ( "а " ) , idx ( "н" ) }
tiles := [ ] TileRecord {
{ Row : 4 , Col : 8 , Letter : "к" } ,
{ Row : 6 , Col : 8 , Letter : "а " } ,
{ Row : 7 , Col : 8 , Letter : "н" } ,
}
return g , tiles
}
// TestEvaluatePlayHonorsSingleWordRule is the regression for the contour bug: under
// the single-word rule the EvaluatePlay preview (the "what would this score, and is
// it legal?" tool) must honour the same rule as SubmitPlay and ignore perpendicular
// cross-words, so a play whose only flaw is invalid cross-words is reported legal and
// scored on its main word alone. Before the fix EvaluatePlay validated under standard
// rules and wrongly rejected it.
func TestEvaluatePlayHonorsSingleWordRule ( t * testing . T ) {
// The main word must be a real Erudit word, so any rejection can only come from
// the (ignored) cross-words rather than the main word itself.
if ok , err := testReg . Lookup ( VariantErudit , testVersion , "кран" ) ; err != nil || ! ok {
t . Fatalf ( "precondition: кран must be in the Erudit dictionary (ok=%v, err=%v)" , ok , err )
}
t . Run ( "single-word rule accepts the cross-invalid play" , func ( t * testing . T ) {
g , tiles := setupSingleWordKran ( t , false )
rec , err := g . EvaluatePlay ( tiles )
if err != nil {
t . Fatalf ( "evaluate under single-word rule: %v" , err )
}
if len ( rec . Words ) != 1 || rec . Words [ 0 ] != "кран" {
t . Errorf ( "words = %v, want [кран] only (cross-words ignored)" , rec . Words )
}
if rec . Dir != Vertical {
t . Errorf ( "dir = %v, want Vertical" , rec . Dir )
}
if rec . Score <= 0 {
t . Errorf ( "score = %d, want positive" , rec . Score )
}
} )
t . Run ( "standard rules reject the same play" , func ( t * testing . T ) {
g , tiles := setupSingleWordKran ( t , true )
if _ , err := g . EvaluatePlay ( tiles ) ; ! errors . Is ( err , ErrIllegalPlay ) {
t . Errorf ( "evaluate under standard rules = %v, want ErrIllegalPlay" , err )
}
} )
t . Run ( "evaluate agrees with submit under the single-word rule" , func ( t * testing . T ) {
g , tiles := setupSingleWordKran ( t , false )
if _ , err := g . SubmitPlay ( tiles ) ; err != nil {
t . Errorf ( "submit under single-word rule: %v" , err )
}
} )
}
// TestSingleWordRuleRobotCandidates proves the robot opponent never trips the same
// cross-word check while searching for its move: its move source, Candidates ->
// GenerateMovesOpts, already honours the rule. Under the single-word rule the bridged
// КРАН (whose cross-words are invalid) appears among the candidates the robot chooses
// from; under standard rules it is correctly absent. The robot submits its pick through
// SubmitPlay (covered above), so this holds both before and after the EvaluatePlay fix —
// the robot never uses EvaluatePlay.
func TestSingleWordRuleRobotCandidates ( t * testing . T ) {
hasKran := func ( cands [ ] MoveRecord ) bool {
for _ , c := range cands {
if len ( c . Words ) > 0 && c . Words [ 0 ] == "кран" {
return true
}
}
return false
}
single , _ := setupSingleWordKran ( t , false )
if ! hasKran ( single . Candidates ( ) ) {
t . Error ( "single-word candidates must include the bridged кран play the robot can pick" )
}
std , _ := setupSingleWordKran ( t , true )
if hasKran ( std . Candidates ( ) ) {
t . Error ( "standard candidates must not include кран (its cross-words are invalid)" )
}
}