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 |