diff --git a/game.go b/game.go index a6a3203..a23d86a 100644 --- a/game.go +++ b/game.go @@ -828,16 +828,37 @@ func (g *Game) Clone() *Game { // clone do not impact the parent ret.rootMove = g.rootMove.Clone() ret.rootMove.cloneChildren(g.rootMove.children) - mlen := len(ret.Moves()) - if mlen == 0 { + if g.currentMove == nil { ret.currentMove = ret.rootMove } else { - ret.currentMove = ret.Moves()[mlen-1] + ret.currentMove = findClonedMove(g.rootMove, ret.rootMove, g.currentMove) + if ret.currentMove == nil { + ret.currentMove = ret.rootMove + } } + ret.pos = ret.currentMove.position return ret } +func findClonedMove(original, clone, target *Move) *Move { + if original == nil || clone == nil || target == nil { + return nil + } + if original == target { + return clone + } + for i, child := range original.children { + if i >= len(clone.children) { + return nil + } + if found := findClonedMove(child, clone.children[i], target); found != nil { + return found + } + } + return nil +} + // Positions returns all positions in the game in the main line. // This includes the starting position and all positions after each move. func (g *Game) Positions() []*Position { diff --git a/game_invariants_test.go b/game_invariants_test.go new file mode 100644 index 0000000..6575d3f --- /dev/null +++ b/game_invariants_test.go @@ -0,0 +1,113 @@ +package chess + +import ( + "strings" + "testing" +) + +func assertGameCurrentPositionInvariant(t *testing.T, g *Game) { + t.Helper() + + if g == nil { + t.Fatal("game is nil") + } + if g.pos == nil { + t.Fatal("game current position is nil") + } + if g.Position() == nil { + t.Fatal("Position() is nil") + } + if g.CurrentPosition() == nil { + t.Fatal("CurrentPosition() is nil") + } + if g.Position().String() != g.pos.String() { + t.Fatalf("Position() = %q, want game current position %q", g.Position(), g.pos) + } + if g.CurrentPosition().String() != g.pos.String() { + t.Fatalf("CurrentPosition() = %q, want game current position %q", g.CurrentPosition(), g.pos) + } + if g.currentMove != nil && g.currentMove.position != nil && g.currentMove.position.String() != g.pos.String() { + t.Fatalf("current move position = %q, want game current position %q", g.currentMove.position, g.pos) + } +} + +func TestGameCurrentPositionInvariantAfterClonePreservesCursor(t *testing.T) { + g := NewGame() + for _, move := range []string{"e4", "e5", "Nf3"} { + if err := g.PushMove(move, nil); err != nil { + t.Fatal(err) + } + } + + if !g.GoBack() { + t.Fatal("expected to navigate back") + } + want := g.CurrentPosition().String() + + clone := g.Clone() + + assertGameCurrentPositionInvariant(t, clone) + if got := clone.CurrentPosition().String(); got != want { + t.Fatalf("clone current position = %q, want %q", got, want) + } +} + +func TestGameCurrentPositionInvariantAfterDirectGameOperations(t *testing.T) { + g := NewGame() + assertGameCurrentPositionInvariant(t, g) + + for _, move := range []string{"e4", "e5", "Nf3"} { + if err := g.PushMove(move, nil); err != nil { + t.Fatal(err) + } + assertGameCurrentPositionInvariant(t, g) + } + + if !g.GoBack() { + t.Fatal("expected to navigate back") + } + assertGameCurrentPositionInvariant(t, g) + + if !g.GoForward() { + t.Fatal("expected to navigate forward") + } + assertGameCurrentPositionInvariant(t, g) +} + +func TestGameCurrentPositionInvariantAfterPGNParse(t *testing.T) { + opt, err := PGN(strings.NewReader("1. e4 e5 2. Nf3 *")) + if err != nil { + t.Fatal(err) + } + + g := NewGame(opt) + + assertGameCurrentPositionInvariant(t, g) +} + +func TestGameCurrentPositionInvariantAfterSplitUsesLineLeaf(t *testing.T) { + g := NewGame() + for _, move := range []string{"e4", "e5", "Nf3"} { + if err := g.PushMove(move, nil); err != nil { + t.Fatal(err) + } + } + + if !g.GoBack() { + t.Fatal("expected to navigate back before adding a variation") + } + if err := g.PushMove("Nc3", nil); err != nil { + t.Fatal(err) + } + + splitGames := g.Split() + if len(splitGames) != 2 { + t.Fatalf("split game count = %d, want 2", len(splitGames)) + } + for _, splitGame := range splitGames { + assertGameCurrentPositionInvariant(t, splitGame) + if !splitGame.IsAtEnd() { + t.Fatalf("split game current position = %q, want leaf position", splitGame.CurrentPosition()) + } + } +}