Thanks to visit codestin.com
Credit goes to github.com

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
152 changes: 148 additions & 4 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
package chess

import (
"bytes"
"errors"
"fmt"
"io"
"strings"
)

// A Outcome is the result of a game.
Expand Down Expand Up @@ -331,7 +334,140 @@
// String implements the fmt.Stringer interface and returns
// the game's PGN.
func (g *Game) String() string {
return g.FEN()
var sb strings.Builder

// Write tag pairs.
for tag, value := range g.tagPairs {
sb.WriteString(fmt.Sprintf("[%s \"%s\"]\n", tag, value))
}
if len(g.tagPairs) > 0 {
sb.WriteString("\n")
}

// Assume g.rootMove is a dummy root (holding the initial position)
// and that its first child is the first actual move.
if g.rootMove != nil && len(g.rootMove.children) > 0 {
writeMoves(g.rootMove, 1, true, &sb, false)
}

// Append the game result.
sb.WriteString(g.Outcome().String()) // outcomeString() returns the result as a string (e.g. "1-0")
return sb.String()
}

// writeMoves recursively writes the PGN-formatted move sequence starting from the given move node into the provided strings.Builder.
// It handles move numbering for white and black moves, encodes moves using algebraic notation based on the appropriate position,
// and appends comments and command annotations if present. The function distinguishes between main line moves and sub-variations;
// when processing a sub-variation, moves are enclosed in parentheses.
//
// Parameters:
// node - pointer to the current move node from which to write moves.
// moveNum - the current move number corresponding to white’s moves.
// isWhite - true if it is white’s move, false if it is black’s move.
// sb - pointer to a strings.Builder where the formatted move notation is appended.
// subVariation - true if the current call is within a sub-variation, affecting formatting details.
//
// The function recurses through the move tree, writing the main line first and then processing any additional variations,
// ensuring that the output adheres to standard PGN conventions. Future enhancements may include support for all NAG values.
func writeMoves(node *Move, moveNum int, isWhite bool, sb *strings.Builder, subVariation bool) {
// If no moves remain, stop.
if node == nil {
return
}

Check warning on line 376 in game.go

View check run for this annotation

Codecov / codecov/patch

game.go#L375-L376

Added lines #L375 - L376 were not covered by tests

var currentMove *Move

// The main line is the first child.
if subVariation {
currentMove = node
} else {
if len(node.children) == 0 {
return // nothing to print if no child exists (should not happen for a proper game)
}

Check warning on line 386 in game.go

View check run for this annotation

Codecov / codecov/patch

game.go#L385-L386

Added lines #L385 - L386 were not covered by tests
currentMove = node.children[0]
}

writeMoveNumber(moveNum, isWhite, subVariation, sb)

// Encode the move using your AlgebraicNotation.
writeMoveEncoding(node, currentMove, sb)

// Append a comment if present.
writeComments(currentMove, sb)

writeCommands(currentMove, sb)

//TODO: Add support for all nags values in the future

// if subvariation is over don't add space
if !subVariation {
sb.WriteString(" ")
} else if len(currentMove.children) > 0 {
sb.WriteString(" ")
}

// Process any variations (children beyond the first).
// In PGN, variations are enclosed in parentheses.
writeVariations(node, moveNum, isWhite, sb)

if len(currentMove.children) > 0 {
var nextMoveNum int
var nextIsWhite bool
if isWhite {
// After white’s move, black plays using the same move number.
nextMoveNum = moveNum
nextIsWhite = false
} else {
// After black’s move, increment move number.
nextMoveNum = moveNum + 1
nextIsWhite = true
}
writeMoves(currentMove, nextMoveNum, nextIsWhite, sb, false)
}
}

func writeMoveNumber(moveNum int, isWhite bool, subVariation bool, sb *strings.Builder) {
if isWhite {
sb.WriteString(fmt.Sprintf("%d. ", moveNum))
} else if subVariation {
sb.WriteString(fmt.Sprintf("%d... ", moveNum))
}
}

func writeMoveEncoding(node *Move, currentMove *Move, sb *strings.Builder) {
if node.Parent() == nil {
sb.WriteString(AlgebraicNotation{}.Encode(node.Position(), currentMove))
} else {
moveStr := AlgebraicNotation{}.Encode(node.Parent().Position(), currentMove)
sb.WriteString(moveStr)
}
}

func writeComments(move *Move, sb *strings.Builder) {
if move.comments != "" {
sb.WriteString(" {" + move.comments + "}")
}
}

func writeCommands(move *Move, sb *strings.Builder) {
if len(move.command) > 0 {
sb.WriteString(" {")
for key, value := range move.command {
sb.WriteString(" [%" + key + " " + value + "]")
}
sb.WriteString(" }")
}
}

func writeVariations(node *Move, moveNum int, isWhite bool, sb *strings.Builder) {
if len(node.children) > 1 {
for i := 1; i < len(node.children); i++ {
variation := node.children[i]
sb.WriteString("(")
writeMoves(variation, moveNum, isWhite, sb, true)
sb.WriteString(") ")
}
}
}

// MarshalText implements the encoding.TextMarshaler interface and
Expand All @@ -340,10 +476,18 @@
return []byte(g.String()), nil
}

// UnmarshalText implements the encoding.TextUnarshaler interface and
// UnmarshalText implements the encoding.TextUnmarshaler interface and
// assumes the data is in the PGN format.
func (g *Game) UnmarshalText(_ []byte) error {
return errors.New("chess: unmarshal text not implemented")
func (g *Game) UnmarshalText(text []byte) error {
r := bytes.NewReader(text)

toGame, err := PGN(r)
if err != nil {
return err
}
toGame(g)

return nil

Check warning on line 490 in game.go

View check run for this annotation

Codecov / codecov/patch

game.go#L481-L490

Added lines #L481 - L490 were not covered by tests
}

// Draw attempts to draw the game by the given method. If the
Expand Down
163 changes: 163 additions & 0 deletions game_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,3 +1011,166 @@ func TestPGNWithEmptyData(t *testing.T) {
t.Fatalf("expected error %v but got %v", ErrNoGameFound, err)
}
}

func TestGameString(t *testing.T) {
tests := []struct {
name string
setup func() *Game
expected string
}{
{
name: "GameStringWithNoMoves",
setup: func() *Game {
return NewGame()
},
expected: "*",
},
{
name: "GameStringWithSingleMove",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
return g
},
expected: "1. e4 *",
},
{
name: "GameStringWithMultipleMoves",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
_ = g.PushMove("e5", nil)
_ = g.PushMove("Nf3", nil)
return g
},
expected: "1. e4 e5 2. Nf3 *",
},
{
name: "GameStringWithComments",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
g.currentMove.comments = "Good move"
return g
},
expected: "1. e4 {Good move} *",
},
{
name: "GameStringWithVariations",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
_ = g.PushMove("e5", nil)
_ = g.PushMove("Nf3", nil)
g.GoBack()
_ = g.PushMove("Nc3", nil)
return g
},
expected: "1. e4 e5 2. Nf3 (2. Nc3) *",
},
{
name: "GameStringWithTags",
setup: func() *Game {
g := NewGame()
g.AddTagPair("Event", "Test Event")
g.AddTagPair("Site", "Test Site")
return g
},
expected: "[Event \"Test Event\"]\n[Site \"Test Site\"]\n\n*",
},
{
name: "GameStringWithWhiteWinResult",
setup: func() *Game {
g := NewGame()
g.outcome = WhiteWon
return g
},
expected: "1-0",
},
{
name: "GameStringWithBlackWinResult",
setup: func() *Game {
g := NewGame()
g.outcome = BlackWon
return g
},
expected: "0-1",
},
{
name: "GameStringWithDrawResult",
setup: func() *Game {
g := NewGame()
g.outcome = Draw
return g
},
expected: "1/2-1/2",
},
{
name: "GameStringWithCommentsAndClock",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
g.currentMove.comments = "Good move"
g.currentMove.SetCommand("clk", "10:00:00")
return g
},
expected: "1. e4 {Good move} { [%clk 10:00:00] } *",
},
{
name: "GameStringWithMultipleNestedVariations",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
_ = g.PushMove("e5", nil)
_ = g.PushMove("Nf3", nil)
g.GoBack()
_ = g.PushMove("Nc3", nil)
g.GoBack()
_ = g.PushMove("d4", nil)
_ = g.PushMove("d5", nil)
_ = g.PushMove("c4", nil)
g.GoBack()
_ = g.PushMove("c3", nil)
g.GoBack()
return g
},
expected: "1. e4 e5 2. Nf3 (2. Nc3) (2. d4 d5 3. c4 (3. c3) ) *",
},
{
name: "GameStringWithVariationsForBlack",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
_ = g.PushMove("e5", nil)
_ = g.PushMove("Nf3", nil)
_ = g.PushMove("Nc6", nil)
_ = g.PushMove("Bb5", nil)
_ = g.PushMove("a6", nil)
g.GoBack()
_ = g.PushMove("d6", nil)
return g
},
expected: "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 (3... d6) *",
},
{
name: "GameStringWithVariationsOnRoot",
setup: func() *Game {
g := NewGame()
_ = g.PushMove("e4", nil)
g.GoBack()
_ = g.PushMove("d4", nil)
return g
},
expected: "1. e4 (1. d4) *",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := tt.setup()
if g.String() != tt.expected {
t.Fatalf("expected %v but got %v", tt.expected, g.String())
}
})
}
}
7 changes: 7 additions & 0 deletions move.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,18 @@
}

func (m *Move) GetCommand(key string) (string, bool) {
if m.command == nil {
m.command = make(map[string]string)
return "", false
}

Check warning on line 73 in move.go

View check run for this annotation

Codecov / codecov/patch

move.go#L70-L73

Added lines #L70 - L73 were not covered by tests
value, ok := m.command[key]
return value, ok
}

func (m *Move) SetCommand(key, value string) {
if m.command == nil {
m.command = make(map[string]string)
}
m.command[key] = value
}

Expand Down
Loading