commit 779b5612bfe6170b306ff1cbdb6d2da17687352a Author: Erik Winter Date: Wed Jan 15 13:59:49 2025 +0100 submitted version diff --git a/README.md b/README.md new file mode 100644 index 0000000..378e5c2 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Tic-Tac-Toe + +A simple implementation of Tic-Tac-Toe that runs in the terminal. + +For information about this game, check [Wikipedia](https://en.wikipedia.org/wiki/Tic-tac-toe). + +Run the game with: + +```bash +go run . +``` + +It uses [escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) to clear the screen after each turn. If this is not supported by your terminal, or if you just don't like them, use the following command to run the game without them: + +```bash +go run . plain +``` diff --git a/board.go b/board.go new file mode 100644 index 0000000..0618b86 --- /dev/null +++ b/board.go @@ -0,0 +1,113 @@ +package main + +import "fmt" + +type Board struct { + // the 3x3 board is stored by concatenating all rows + // + // 0 1 2 + // 3 4 5 + // 6 7 8 + // + // becomes: + // + // 0 1 2 3 4 5 6 7 8 + // + // each square contains either one of the player marks, or a space + squares [9]string +} + +func NewBoard() *Board { + s := [9]string{} + for i := range s { + s[i] = " " + } + return &Board{ + squares: s, + } +} + +// Mark puts a mark in a square. +// returns false if the square was already occupied +func (b *Board) Mark(sq int, mark string) bool { + if b.squares[sq] != " " { + return false + } + + b.squares[sq] = mark + return true +} + +func (b *Board) Available() []int { + av := make([]int, 0) + for p, m := range b.squares { + if m == " " { + av = append(av, p) + } + } + + return av +} + +func (b *Board) Full() bool { + return len(b.Available()) == 0 +} + +func (b *Board) Winner() (string, bool) { + // someone has won when the board features identical marks on all these places + winConfs := [][3]int{ + {0, 1, 2}, // horizontal rows + {3, 4, 5}, + {6, 7, 8}, + + {0, 3, 6}, // vertical rows + {1, 4, 7}, + {2, 5, 8}, + + {0, 4, 8}, // diagonals + {2, 4, 6}, + } + + for _, w := range winConfs { + // first determine what mark is on the first position + m := b.squares[w[0]] + if m == " " { + continue + } + // then check whether the others are the same + if m == b.squares[w[1]] && m == b.squares[w[2]] { + return m, true + } + } + + return "", false +} + +func (b *Board) Render(useEsc bool) string { + sqs := make([]string, 0, 8) + for _, sq := range b.squares { + sqStr := sq + if useEsc { + // make the marks bold and white + sqStr = fmt.Sprintf("\x1b[1;37m%s\x1b[0m", sqStr) + } + sqs = append(sqs, sqStr) + } + + return fmt.Sprintf(` -0--- -1--- -2--- +| | | | +| %s | %s | %s | +| | | | + -3--- -4--- -5--- +| | | | +| %s | %s | %s | +| | | | + -6--- -7--- -8--- +| | | | +| %s | %s | %s | +| | | | + ----- ----- ----- +`, sqs[0], sqs[1], sqs[2], + sqs[3], sqs[4], sqs[5], + sqs[6], sqs[7], sqs[8]) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1ebd34d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module tictactoe + +go 1.23.3 diff --git a/main.go b/main.go new file mode 100644 index 0000000..d57e848 --- /dev/null +++ b/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + useEscapeCodes := true + if len(os.Args) == 2 && os.Args[1] == "plain" { + useEscapeCodes = false + } + + game := NewGame(NewHuman("X"), NewHuman("O"), useEscapeCodes) + for { + if done := game.Turn(); done { + os.Exit(0) + } + } +} + +type Game struct { + turns int + players [2]Player + board *Board + useEsc bool +} + +func NewGame(p1, p2 Player, useEsc bool) *Game { + return &Game{ + players: [2]Player{p1, p2}, + board: NewBoard(), + useEsc: useEsc, + } +} + +// Turn returns true when the game is finished. +func (g *Game) Turn() bool { + g.RenderBoard() + + // get next move + curPl := g.turns % 2 + fmt.Printf("Turn %d: player %d (%s) can make a move\n", g.turns, curPl, g.players[curPl].Mark()) + if cont := g.players[curPl].MakeMove(g.board); !cont { + fmt.Println("Maybe some other time then. Bye!") + return true + } + + // check result + if _, ok := g.board.Winner(); ok { + g.RenderBoard() + fmt.Printf("Congratulations player %d, you win!\n", curPl) + return true + } + if g.board.Full() { + g.RenderBoard() + fmt.Println("Stalemate! Try again...") + return true + } + + g.turns++ + + return false +} + +func (g *Game) RenderBoard() { + if g.useEsc { + // clear screen first + fmt.Print("\033[H\033[2J") + } + fmt.Println(g.board.Render(g.useEsc)) +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..2106456 --- /dev/null +++ b/player.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "strconv" + "strings" +) + +type Player interface { + Mark() string + MakeMove(b *Board) bool +} + +type Human struct { + mark string +} + +func NewHuman(m string) *Human { + return &Human{mark: m} +} + +func (h *Human) Mark() string { return h.mark } + +// MakeMove marks the board +// returns false if the player wants to stop +func (h *Human) MakeMove(b *Board) bool { + for { + sq, stop, err := AskInput() + if err != nil { + fmt.Printf("That didn't work: %v\n", err) + fmt.Println("Try again, or press ctrl-c to abort.") + continue + } + if stop { + return false + } + + if ok := b.Mark(sq, h.mark); !ok { + fmt.Println("That square was already taken. Sorry.") + + av := make([]string, 0) + for _, a := range b.Available() { + av = append(av, fmt.Sprintf("%d", a)) + } + fmt.Printf("Open squares are: %s\n", strings.Join(av, ", ")) + continue + } + + return true + } +} + +// AskInput waits for user input. +// returns the square, a request to stop, or an error +func AskInput() (int, bool, error) { + fmt.Println("Enter 0-8 to mark a square, or q to quit:") + + var input string + count, err := fmt.Scan(&input) + if err != nil { + return 0, false, err + } + if count != 1 { + return 0, false, fmt.Errorf("one can only mark a single square per turn") + } + if input == "q" { + return 0, true, nil + } + sq, err := strconv.Atoi(input) + if err != nil || sq < 0 || sq > 8 { + return 0, false, fmt.Errorf("squares are indicated by the numbers 0 to 8") + } + + return sq, false, nil +}