tictactoe_bot_dynamic.go
· 4.8 KiB · Go
Raw
package main
import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
)
const emptyCellName = " "
type User struct {
name string
isBot bool
}
type Board struct {
board [][]*User
winningUser *User
combinations [][]**User
users []User
}
func (b Board) String() string {
size := len(b.board)
rowStrings := make([]string, size+1)
columnIndices := make([]string, size)
for i := 0; i < size; i++ {
rowIndex := strconv.Itoa(i)
if i < 10 {
rowIndex = " " + rowIndex
}
columnIndices[i] = rowIndex
}
rowStrings[0] = "/" + strings.Join(columnIndices, "")
for i, row := range b.board {
rowIndex := strconv.Itoa(i)
if i < 10 {
rowIndex += " "
}
columnStrings := make([]string, size)
for j, c := range row {
if c == nil {
columnStrings[j] = emptyCellName
} else {
columnStrings[j] = c.name
}
}
rowStrings[i+1] = rowIndex + strings.Join(columnStrings, "|")
}
return strings.Join(rowStrings, "\n")
}
func changeCellUserInput(board *[][]*User, user *User) {
var row int
for {
fmt.Print("row: ")
fmt.Scanln(&row)
if 0 <= row && row < len(*board) {
break
}
}
var col int
for {
fmt.Print("col: ")
fmt.Scanln(&col)
if 0 <= col && col < len(*board) {
break
}
}
(*board)[row][col] = user
}
func getWinningCombinations(board *[][]*User) [][]**User {
// there are 2 * size + 2 winning combinations in total
winningCombinations := make([][]**User, len(*board)*2+2)
// size row wins | size col wins
for i, _ := range *board {
row := make([]**User, len(*board))
col := make([]**User, len(*board))
for j, _ := range *board {
row[j] = &(*board)[i][j]
col[j] = &(*board)[j][i]
}
winningCombinations[i*2] = row
winningCombinations[i*2+1] = col
}
// 2 vertical wins
a := make([]**User, len(*board))
b := make([]**User, len(*board))
for i, _ := range *board {
a[i] = &(*board)[i][i]
b[i] = &(*board)[i][len(*board)-i-1]
}
winningCombinations[len(*board)*2] = a
winningCombinations[len(*board)*2+1] = b
return winningCombinations
}
type combinationStats struct {
user *User
totalCount int
userCount int
emptyCells []**User
}
func (b *Board) computeCombinationStats(user *User) []combinationStats {
newCombinations := [][]**User{}
cs := []combinationStats{}
for _, combination := range b.combinations {
c := combinationStats{}
allUsersMap := make(map[*User]int)
for _, cell := range combination {
c.totalCount++
if *cell == nil {
c.emptyCells = append(c.emptyCells, cell)
} else {
allUsersMap[*cell]++
}
}
allUsers := slices.Collect(maps.Keys(allUsersMap))
if len(allUsers) == 1 {
c.user = allUsers[0]
c.userCount = allUsersMap[allUsers[0]]
cs = append(cs, c)
newCombinations = append(newCombinations, combination)
if c.userCount == c.totalCount {
b.winningUser = slices.Collect(maps.Keys(allUsersMap))[0]
}
} else if len(allUsers) == 0 {
cs = append(cs, c)
newCombinations = append(newCombinations, combination)
}
}
b.combinations = newCombinations
return cs
}
func changeCellBotInput(combinations []combinationStats, user *User) {
// now count the still possible wins each pointer has
cellScores := make(map[**User]int)
for _, c := range combinations {
score := 0
if c.user == nil || c.user == user {
score = 1 + c.userCount
} else {
score = 1 + c.userCount
}
for _, emptyCell := range c.emptyCells {
cellScores[emptyCell] += score
}
}
bestCellScore := 0
var bestCell **User
for cell, cellScore := range cellScores {
if cellScore > bestCellScore {
bestCellScore = cellScore
bestCell = cell
}
}
*bestCell = user
}
func (b Board) Play() {
turn := 0
for {
currentUser := &b.users[turn%len(b.users)]
fmt.Println()
fmt.Println("it's the turn of player " + currentUser.name)
fmt.Println(b.String())
cs := b.computeCombinationStats(currentUser)
if b.winningUser != nil {
fmt.Println()
if b.winningUser.isBot {
fmt.Println("THE BOT " + b.winningUser.name + " WON!!!")
} else {
fmt.Println("THE USER " + b.winningUser.name + " WON!!!")
}
break
}
if len(b.combinations) == 0 {
fmt.Println()
fmt.Println("GAME OVER. NEITHER ONE WON")
break
}
if currentUser.isBot {
changeCellBotInput(cs, currentUser)
} else {
fmt.Println()
changeCellUserInput(&b.board, currentUser)
}
turn++
}
}
func createBoardGrid(size int) [][]*User {
board := make([][]*User, size)
for r := 0; r < size; r++ {
board[r] = make([]*User, size)
}
return board
}
func NewBoard(size int, users []User) Board {
board := createBoardGrid(size)
return Board{
board: board,
combinations: getWinningCombinations(&board),
users: users,
}
}
func main() {
board := NewBoard(3, []User{
{name: "X", isBot: true},
{name: "O", isBot: false},
})
board.Play()
}
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "maps" |
| 6 | "slices" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | const emptyCellName = " " |
| 12 | |
| 13 | type User struct { |
| 14 | name string |
| 15 | isBot bool |
| 16 | } |
| 17 | |
| 18 | type Board struct { |
| 19 | board [][]*User |
| 20 | winningUser *User |
| 21 | |
| 22 | combinations [][]**User |
| 23 | |
| 24 | users []User |
| 25 | } |
| 26 | |
| 27 | func (b Board) String() string { |
| 28 | size := len(b.board) |
| 29 | rowStrings := make([]string, size+1) |
| 30 | |
| 31 | columnIndices := make([]string, size) |
| 32 | for i := 0; i < size; i++ { |
| 33 | rowIndex := strconv.Itoa(i) |
| 34 | if i < 10 { |
| 35 | rowIndex = " " + rowIndex |
| 36 | } |
| 37 | columnIndices[i] = rowIndex |
| 38 | } |
| 39 | rowStrings[0] = "/" + strings.Join(columnIndices, "") |
| 40 | |
| 41 | for i, row := range b.board { |
| 42 | rowIndex := strconv.Itoa(i) |
| 43 | if i < 10 { |
| 44 | rowIndex += " " |
| 45 | } |
| 46 | |
| 47 | columnStrings := make([]string, size) |
| 48 | for j, c := range row { |
| 49 | if c == nil { |
| 50 | columnStrings[j] = emptyCellName |
| 51 | } else { |
| 52 | columnStrings[j] = c.name |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | rowStrings[i+1] = rowIndex + strings.Join(columnStrings, "|") |
| 57 | } |
| 58 | |
| 59 | return strings.Join(rowStrings, "\n") |
| 60 | } |
| 61 | |
| 62 | func changeCellUserInput(board *[][]*User, user *User) { |
| 63 | var row int |
| 64 | for { |
| 65 | fmt.Print("row: ") |
| 66 | fmt.Scanln(&row) |
| 67 | |
| 68 | if 0 <= row && row < len(*board) { |
| 69 | break |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | var col int |
| 74 | for { |
| 75 | fmt.Print("col: ") |
| 76 | fmt.Scanln(&col) |
| 77 | |
| 78 | if 0 <= col && col < len(*board) { |
| 79 | break |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | (*board)[row][col] = user |
| 84 | } |
| 85 | |
| 86 | func getWinningCombinations(board *[][]*User) [][]**User { |
| 87 | // there are 2 * size + 2 winning combinations in total |
| 88 | winningCombinations := make([][]**User, len(*board)*2+2) |
| 89 | |
| 90 | // size row wins | size col wins |
| 91 | for i, _ := range *board { |
| 92 | row := make([]**User, len(*board)) |
| 93 | col := make([]**User, len(*board)) |
| 94 | |
| 95 | for j, _ := range *board { |
| 96 | row[j] = &(*board)[i][j] |
| 97 | col[j] = &(*board)[j][i] |
| 98 | } |
| 99 | |
| 100 | winningCombinations[i*2] = row |
| 101 | winningCombinations[i*2+1] = col |
| 102 | } |
| 103 | |
| 104 | // 2 vertical wins |
| 105 | a := make([]**User, len(*board)) |
| 106 | b := make([]**User, len(*board)) |
| 107 | for i, _ := range *board { |
| 108 | a[i] = &(*board)[i][i] |
| 109 | b[i] = &(*board)[i][len(*board)-i-1] |
| 110 | } |
| 111 | winningCombinations[len(*board)*2] = a |
| 112 | winningCombinations[len(*board)*2+1] = b |
| 113 | |
| 114 | return winningCombinations |
| 115 | } |
| 116 | |
| 117 | type combinationStats struct { |
| 118 | user *User |
| 119 | |
| 120 | totalCount int |
| 121 | userCount int |
| 122 | |
| 123 | emptyCells []**User |
| 124 | } |
| 125 | |
| 126 | func (b *Board) computeCombinationStats(user *User) []combinationStats { |
| 127 | newCombinations := [][]**User{} |
| 128 | cs := []combinationStats{} |
| 129 | |
| 130 | for _, combination := range b.combinations { |
| 131 | c := combinationStats{} |
| 132 | allUsersMap := make(map[*User]int) |
| 133 | |
| 134 | for _, cell := range combination { |
| 135 | c.totalCount++ |
| 136 | |
| 137 | if *cell == nil { |
| 138 | c.emptyCells = append(c.emptyCells, cell) |
| 139 | } else { |
| 140 | allUsersMap[*cell]++ |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | allUsers := slices.Collect(maps.Keys(allUsersMap)) |
| 145 | if len(allUsers) == 1 { |
| 146 | c.user = allUsers[0] |
| 147 | c.userCount = allUsersMap[allUsers[0]] |
| 148 | cs = append(cs, c) |
| 149 | newCombinations = append(newCombinations, combination) |
| 150 | |
| 151 | if c.userCount == c.totalCount { |
| 152 | b.winningUser = slices.Collect(maps.Keys(allUsersMap))[0] |
| 153 | } |
| 154 | } else if len(allUsers) == 0 { |
| 155 | cs = append(cs, c) |
| 156 | newCombinations = append(newCombinations, combination) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | b.combinations = newCombinations |
| 161 | return cs |
| 162 | } |
| 163 | |
| 164 | func changeCellBotInput(combinations []combinationStats, user *User) { |
| 165 | // now count the still possible wins each pointer has |
| 166 | cellScores := make(map[**User]int) |
| 167 | for _, c := range combinations { |
| 168 | score := 0 |
| 169 | if c.user == nil || c.user == user { |
| 170 | score = 1 + c.userCount |
| 171 | } else { |
| 172 | score = 1 + c.userCount |
| 173 | } |
| 174 | |
| 175 | for _, emptyCell := range c.emptyCells { |
| 176 | cellScores[emptyCell] += score |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | bestCellScore := 0 |
| 181 | var bestCell **User |
| 182 | for cell, cellScore := range cellScores { |
| 183 | if cellScore > bestCellScore { |
| 184 | bestCellScore = cellScore |
| 185 | bestCell = cell |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | *bestCell = user |
| 190 | } |
| 191 | |
| 192 | func (b Board) Play() { |
| 193 | turn := 0 |
| 194 | for { |
| 195 | currentUser := &b.users[turn%len(b.users)] |
| 196 | fmt.Println() |
| 197 | fmt.Println("it's the turn of player " + currentUser.name) |
| 198 | fmt.Println(b.String()) |
| 199 | |
| 200 | cs := b.computeCombinationStats(currentUser) |
| 201 | |
| 202 | if b.winningUser != nil { |
| 203 | fmt.Println() |
| 204 | if b.winningUser.isBot { |
| 205 | fmt.Println("THE BOT " + b.winningUser.name + " WON!!!") |
| 206 | } else { |
| 207 | fmt.Println("THE USER " + b.winningUser.name + " WON!!!") |
| 208 | } |
| 209 | break |
| 210 | } |
| 211 | |
| 212 | if len(b.combinations) == 0 { |
| 213 | fmt.Println() |
| 214 | fmt.Println("GAME OVER. NEITHER ONE WON") |
| 215 | break |
| 216 | } |
| 217 | |
| 218 | if currentUser.isBot { |
| 219 | changeCellBotInput(cs, currentUser) |
| 220 | } else { |
| 221 | fmt.Println() |
| 222 | changeCellUserInput(&b.board, currentUser) |
| 223 | } |
| 224 | |
| 225 | turn++ |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | func createBoardGrid(size int) [][]*User { |
| 230 | board := make([][]*User, size) |
| 231 | for r := 0; r < size; r++ { |
| 232 | board[r] = make([]*User, size) |
| 233 | } |
| 234 | return board |
| 235 | } |
| 236 | |
| 237 | func NewBoard(size int, users []User) Board { |
| 238 | board := createBoardGrid(size) |
| 239 | return Board{ |
| 240 | board: board, |
| 241 | combinations: getWinningCombinations(&board), |
| 242 | users: users, |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | func main() { |
| 247 | board := NewBoard(3, []User{ |
| 248 | {name: "X", isBot: true}, |
| 249 | {name: "O", isBot: false}, |
| 250 | }) |
| 251 | board.Play() |
| 252 | } |
| 253 |