1- // Package tictactoe implements a classic Tic-tac-toe game where two players
2- // take turns marking spaces on a 3x3 grid. Players alternate placing X's and O's
3- // on the board until either one player wins by placing three marks in a row,
4- // column, or diagonal, or the game ends in a draw when the board is full.
1+ // Package tictactoe implements a classic Tic-tac-toe game where players can play
2+ // against another player locally or against a computer opponent.
53package tictactoe
64
75import (
86 "errors"
97 "fmt"
8+ "math/rand"
9+ "time"
10+
11+ "github.com/charmbracelet/lipgloss"
12+ )
13+
14+ // GameMode represents the type of game being played (local multiplayer or against computer)
15+ type GameMode int
16+
17+ const (
18+ // LocalGame represents a game played between two local players
19+ LocalGame GameMode = iota
20+ // ComputerGame represents a game played against the computer
21+ ComputerGame
22+ )
23+
24+ var (
25+ // Use accessible colors - blue for X and yellow for O
26+ // These colors have good contrast and are distinguishable for most types of color blindness
27+ xStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("45" )) // bright blue
28+ oStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("226" )) // bright yellow
1029)
1130
1231// Prompter defines an interface for getting user input
@@ -26,17 +45,25 @@ type Board [3][3]string
2645
2746// Game represents the current state of a Tic-tac-toe game.
2847type Game struct {
29- board Board // The game board storing player moves ("X" or "O", or empty for unplayed)
30- CurrentPlayer string // CurrentPlayer indicates whose turn it is ("X" or "O")
48+ board Board // The game board storing player moves ("X" or "O", or empty for unplayed)
49+ CurrentPlayer string // CurrentPlayer indicates whose turn it is ("X" or "O")
50+ Mode GameMode // Mode indicates if playing against computer or local player
51+ ComputerMark string // ComputerMark stores which mark (X/O) the computer is using
3152}
3253
3354// NewGame creates and initializes a new Tic-tac-toe game with an empty board.
3455// Setting as X always plays first.
35- func NewGame () * Game {
36- return & Game {
56+ func NewGame (mode GameMode ) * Game {
57+ game := & Game {
3758 board : Board {},
3859 CurrentPlayer : "X" ,
60+ Mode : mode ,
61+ }
62+ if mode == ComputerGame {
63+ // Computer always plays as O
64+ game .ComputerMark = "O"
3965 }
66+ return game
4067}
4168
4269// MakeMove attempts to place the current player's mark at the specified position.
@@ -113,8 +140,8 @@ func (g *Game) IsBoardFull() bool {
113140
114141// String returns a formatted string representation of the current board state.
115142// Empty squares are shown as numbers 1-9 for position selection, making it
116- // easier for players to choose their moves. Played squares show the player's mark.
117- // The board is displayed with column separators (|) and row separators (- ).
143+ // easier for players to choose their moves. Played squares show the player's mark
144+ // in their respective colors (blue for X, yellow for O ).
118145func (g * Game ) String () string {
119146 result := "\n "
120147 position := 1
@@ -125,11 +152,16 @@ func (g *Game) String() string {
125152 result += " "
126153 }
127154
128- // Show position number for empty squares, otherwise show the player's mark
155+ // Show position number for empty squares, otherwise show the colored player's mark
129156 if g.board [rowIndex ][columnIndex ] == "" {
130157 result += fmt .Sprintf ("%d" , position )
131158 } else {
132- result += g.board [rowIndex ][columnIndex ]
159+ mark := g.board [rowIndex ][columnIndex ]
160+ if mark == "X" {
161+ result += xStyle .Render ("X" )
162+ } else {
163+ result += oStyle .Render ("O" )
164+ }
133165 }
134166
135167 // Add column separators except for the last column
@@ -218,3 +250,82 @@ func GetPlayerMove(prompter Prompter, game GameInterface) (rowIndex, columnIndex
218250
219251 return rowIndex , columnIndex , nil
220252}
253+
254+ // IsComputerTurn returns true if it's the computer's turn in a computer game.
255+ // This will only return true if the game mode is ComputerGame and the current
256+ // player matches the computer's mark.
257+ func (g * Game ) IsComputerTurn () bool {
258+ return g .Mode == ComputerGame && g .CurrentPlayer == g .ComputerMark
259+ }
260+
261+ // GetComputerMove determines the computer's next move using a simple strategy:
262+ // 1. Win if possible
263+ // 2. Block opponent's winning move if possible
264+ // 3. Take center if available
265+ // 4. Take a random corner if available
266+ // 5. Take any available space
267+ // Returns row and column indices for the chosen move.
268+ func (g * Game ) GetComputerMove () (rowIndex , columnIndex int ) {
269+ // Try to win if possible
270+ if move := g .findWinningMove (g .ComputerMark ); move != nil {
271+ return move [0 ], move [1 ]
272+ }
273+
274+ // Block opponent's winning move if possible
275+ playerMark := "X" // Player is always X
276+ if move := g .findWinningMove (playerMark ); move != nil {
277+ return move [0 ], move [1 ]
278+ }
279+
280+ // Take center if available
281+ if g .board [1 ][1 ] == "" {
282+ return 1 , 1
283+ }
284+
285+ // Take a corner if available
286+ corners := [][2 ]int {{0 , 0 }, {0 , 2 }, {2 , 0 }, {2 , 2 }}
287+ rand .Seed (time .Now ().UnixNano ())
288+ rand .Shuffle (len (corners ), func (i , j int ) {
289+ corners [i ], corners [j ] = corners [j ], corners [i ]
290+ })
291+ for _ , corner := range corners {
292+ if g .board [corner [0 ]][corner [1 ]] == "" {
293+ return corner [0 ], corner [1 ]
294+ }
295+ }
296+
297+ // Take any available space
298+ for rowIndex := 0 ; rowIndex < 3 ; rowIndex ++ {
299+ for columnIndex := 0 ; columnIndex < 3 ; columnIndex ++ {
300+ if g.board [rowIndex ][columnIndex ] == "" {
301+ return rowIndex , columnIndex
302+ }
303+ }
304+ }
305+
306+ return - 1 , - 1
307+ }
308+
309+ // findWinningMove checks if there's a winning move available for the given mark.
310+ // It simulates placing the mark in each empty position and checks if it results in a win.
311+ // Returns the [row, column] indices of the winning move, or nil if no winning move exists.
312+ func (g * Game ) findWinningMove (mark string ) []int {
313+ // Try each empty position
314+ for rowIndex := 0 ; rowIndex < 3 ; rowIndex ++ {
315+ for columnIndex := 0 ; columnIndex < 3 ; columnIndex ++ {
316+ if g.board [rowIndex ][columnIndex ] == "" {
317+ // Try the move
318+ g.board [rowIndex ][columnIndex ] = mark
319+ // Check if it's a winning move
320+ if g .GetWinner () == mark {
321+ // Undo the move and return the position
322+ g.board [rowIndex ][columnIndex ] = ""
323+ return []int {rowIndex , columnIndex }
324+ }
325+ // Undo the move
326+ g.board [rowIndex ][columnIndex ] = ""
327+ }
328+ }
329+ }
330+ return nil
331+ }
0 commit comments