Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,35 @@ if err != nil {
defer f.Close()

scanner := chess.NewScanner(f)
for scanner.Scan() {
game := scanner.Next()
// Read all games
for scanner.HasNext() {
game, err := scanner.ParseNext()
if err != nil {
log.Fatal("Failed to parse game: %v", err)
}
fmt.Println(game.GetTagPair("Site"))
// Output &{Site https://lichess.org/8jb5kiqw}
}
```

#### Scan PGN expanding all variations

To expand every variation into a distinct Game:

```go
f, err := os.Open("lichess_db_standard_rated_2013-01.pgn")
if err != nil {
panic(err)
}
defer f.Close()

scanner := chess.NewScanner(f, chess.WithExpandVariations())
// Read all games
for scanner.HasNext() {
game, err := scanner.ParseNext()
if err != nil {
log.Fatal("Failed to parse game: %v", err)
}
fmt.Println(game.GetTagPair("Site"))
// Output &{Site https://lichess.org/8jb5kiqw}
}
Expand Down
64 changes: 64 additions & 0 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -897,3 +897,67 @@ func (g *Game) updatePosition(move *Move) {
move.position = newPos
}
}

// Split takes a Game with a main line and 0 or more variations and returns a
// slice of Games (one for each variation), each containing exactly only a main
// line and 0 variations
func (g *Game) Split() []*Game {
// Collect all move paths starting from the root's children
var paths [][]*Move
for _, m := range g.rootMove.children {
for _, p := range collectPaths(m) {
paths = append(paths, p)
}
}

// Build a Game for each path
var games []*Game
for _, path := range paths {
newG := g.buildOneGameFromPath(path)
games = append(games, newG)
}

return games
}

// collectPaths returns all paths from the given move to each leaf node.
// Each path is represented as a slice of *Move, starting with the given node
// and ending with a leaf (a move with no children).
func collectPaths(node *Move) [][]*Move {
if node == nil {
return nil
}
// If leaf, return a single path containing this node
if len(node.children) == 0 {
return [][]*Move{{node}}
}
// Otherwise, collect paths from each child and prepend this node
var paths [][]*Move
for _, c := range node.children {
childPaths := collectPaths(c)
for _, p := range childPaths {
path := append([]*Move{node}, p...)
paths = append(paths, path)
}
}
return paths
}

func (g *Game) buildOneGameFromPath(path []*Move) *Game {
rootMove := &Move{position: g.rootMove.position.copy()}
cur := rootMove

for _, m := range path {
child := m.Clone()
child.parent = cur

cur.children = []*Move{child}
cur = child
}

newG := g.Clone()
newG.rootMove = rootMove
newG.currentMove = cur

return newG
}
54 changes: 54 additions & 0 deletions game_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,3 +1220,57 @@ func FuzzTestPushNotationMove(f *testing.F) {
_ = game.PushNotationMove(move, notation, nil)
})
}

func validateSplit(t *testing.T, origPgn string, expectedLastLines []string) {
reader := strings.NewReader(origPgn)
scanner := NewScanner(reader)
game, err := scanner.ParseNext()
if err != nil {
t.Fatalf("fail to parse game: %s", err.Error())
}

if game == nil {
t.Fatalf("game is nil")
}

splitGames := game.Split()
if len(expectedLastLines) != len(splitGames) {
t.Fatalf("expected %v split games but got %v", len(expectedLastLines),
len(splitGames))
}

for idx, g := range game.Split() {
lines := strings.Split(g.String(), "\n")
if len(lines) == 0 {
t.Fatalf("split game %v output blank", idx)
}

lastLine := lines[len(lines)-1]
if lastLine != expectedLastLines[idx] {
t.Errorf("game output not correct\n\tExpected:'%v'\n\tGot: '%v'\n",
expectedLastLines[idx], lastLine)
}
}
}

func TestGameSplitVar(t *testing.T) {
expectedLastLines := []string{
"1. e4 e5 2. Nf3 Nc6 3. d4 exd4 4. Nxd4 *",
"1. e4 e5 2. Nc3 Nf6 3. f4 *",
"1. e4 d6 2. d4 Nf6 3. Nc3 e5 4. dxe5 dxe5 5. Qxd8+ Kxd8 *",
"1. e4 d6 2. d4 Nf6 3. Nc3 e5 4. Nf3 Nbd7 *",
"1. e3 e5 *",
}

pgn := mustParsePGN("fixtures/pgns/variations.pgn")
validateSplit(t, pgn, expectedLastLines)
}

func TestGameSplitNoVar(t *testing.T) {
expectedLastLines := []string{
"1. e4 e5 2. Nf3 Nc6 *",
}

pgn := "[Event \"SomeEvent\"]\n1. e4 e5 2. Nf3 Nc6\n\n"
validateSplit(t, pgn, expectedLastLines)
}
27 changes: 27 additions & 0 deletions move.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,30 @@ func (m *Move) Ply() int {
// After the move, it's Black's turn, so the move was by White
return (moveNumber)*2 + 0
}

// Clone returns a deep copy of a move.
//
// Per-field exceptions:
//
// parent: not copied; the clone'd move has no parent
// children: not copied; the clone'd move has no children
func (m *Move) Clone() *Move {
ret := &Move{}
ret.parent = nil
ret.position = m.position.copy()
ret.nag = m.nag
ret.comments = m.comments
ret.children = make([]*Move, 0)
ret.number = m.number
ret.tags = m.tags
ret.s1 = m.s1
ret.s2 = m.s2
ret.promo = m.promo

ret.command = make(map[string]string)
for k, v := range m.command {
ret.command[k] = v
}

return ret
}
65 changes: 65 additions & 0 deletions move_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,68 @@ func moveIsValid(pos *Position, m *Move, useTags bool) bool {
}
return false
}

func assertMovesAreEqual(t *testing.T, m1, m2 *Move) {
if m1.parent != m2.parent {
t.Fatalf("cloned mv %s parent is not the same", m1)
}
if m1.position.String() != m2.position.String() {
t.Fatalf("cloned mv %s position is not the same", m1)
}
if m1.nag != m2.nag {
t.Fatalf("cloned mv %s nag is not the same", m1)
}
if m1.comments != m2.comments {
t.Fatalf("cloned mv %s comments is not the same", m1)
}
if m1.number != m2.number {
t.Fatalf("cloned mv %s number is not the same", m1)
}
if m1.tags != m2.tags {
t.Fatalf("cloned mv %s tags is not the same", m1)
}
if m1.s1 != m2.s1 {
t.Fatalf("cloned mv %s s1 is not the same", m1)
}
if m1.s2 != m2.s2 {
t.Fatalf("cloned mv %s s2 is not the same", m1)
}
if m1.promo != m2.promo {
t.Fatalf("cloned mv %s s2 is not the same", m1)
}

if len(m1.command) != len(m2.command) {
t.Fatalf("cloned mv %s len(command) is not the same", m1)
} else {
for k, v1 := range m1.command {
v2, ok := m2.command[k]
if !ok || v2 != v1 {
t.Fatalf("cloned mv %s command[%v] is not the same", m1, k)
}
}
}
if len(m1.children) != len(m2.children) {
t.Fatalf("cloned mv %s len(command) is not the same", m1)
} else {
for idx, c1 := range m1.children {
c2 := m2.children[idx]
assertMovesAreEqual(t, c1, c2)
}
}
}

func TestMoveClone(t *testing.T) {
for _, mt := range validMoves {
mt.m.position = mt.pos
clonedM1 := mt.m.Clone()
assertMovesAreEqual(t, mt.m, clonedM1)
clonedM1.SetCommand("foo", "bar")
clonedM2 := clonedM1.Clone()
assertMovesAreEqual(t, clonedM1, clonedM2)
clonedM1.SetCommand("foo", "bar modified")
fooVal, ok := clonedM2.GetCommand("foo")
if !ok || fooVal != "bar" {
t.Fatalf("cloned mv %s is not a deep copy", clonedM2)
}
}
}
Loading
Loading