submitted version

This commit is contained in:
Erik Winter 2025-01-15 13:59:49 +01:00
commit 779b5612bf
5 changed files with 280 additions and 0 deletions

17
README.md Normal file
View File

@ -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
```

113
board.go Normal file
View File

@ -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])
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module tictactoe
go 1.23.3

72
main.go Normal file
View File

@ -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))
}

75
player.go Normal file
View File

@ -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
}