diff --git a/game.go b/game.go index 986d0f3..e6ce9c8 100644 --- a/game.go +++ b/game.go @@ -22,8 +22,11 @@ Example usage: package chess import ( + "bytes" "errors" + "fmt" "io" + "strings" ) // A Outcome is the result of a game. @@ -331,7 +334,140 @@ func (g *Game) FEN() string { // 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 + } + + 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) + } + 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 @@ -340,10 +476,18 @@ func (g *Game) MarshalText() ([]byte, error) { 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 } // Draw attempts to draw the game by the given method. If the diff --git a/game_test.go b/game_test.go index 8ffcf13..a7c8d68 100644 --- a/game_test.go +++ b/game_test.go @@ -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()) + } + }) + } +} diff --git a/move.go b/move.go index 362c211..b22c8a2 100644 --- a/move.go +++ b/move.go @@ -67,11 +67,18 @@ func (m *Move) addTag(tag MoveTag) { } func (m *Move) GetCommand(key string) (string, bool) { + if m.command == nil { + m.command = make(map[string]string) + return "", false + } 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 }