Skip to content

Commit

Permalink
pcplayer engine: add MinMax Algorithm implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gucio321 committed Jan 16, 2024
1 parent 3b9bd2b commit b94864c
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 14 deletions.
2 changes: 1 addition & 1 deletion internal/terminalgame/gameimpl/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"log"
"os"

"github.com/gucio321/go-clear"

Check failure on line 9 in internal/terminalgame/gameimpl/game.go

View workflow job for this annotation

GitHub Actions / golangci

File is not `goimports`-ed with -local github.com/gucio321/tic-tac-go (goimports)
"github.com/gucio321/tic-tac-go/pkg/core/board/letter"

"github.com/gucio321/go-clear"
"github.com/gucio321/terminalmenu/pkg/menuutils"

"github.com/gucio321/tic-tac-go/pkg/game"
Expand Down
18 changes: 12 additions & 6 deletions internal/terminalgame/menu/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import (
"strconv"
"strings"

"github.com/gucio321/go-clear"

"github.com/jaytaylor/html2text"
"github.com/pkg/browser"
"github.com/russross/blackfriday"

"github.com/gravestench/osinfo"

"github.com/gucio321/go-clear"
terminalmenu "github.com/gucio321/terminalmenu/pkg"
"github.com/gucio321/terminalmenu/pkg/menuutils"

Expand Down Expand Up @@ -57,7 +56,8 @@ func New(readme []byte) *Menu {
func (m *Menu) Run() {
err := <-terminalmenu.Create("Tic-Tac-Go", true).
MainPage("Main Menu").
Item("PvC game", m.runPVC).
Item("PvC game (Standard Algorithm)", m.runPVCOriginal).
Item("PvC game (Min-Max Algorithm - slower)", m.runPVCMinMax).
Item("PvP game", m.runPVP).
Item("Demo", m.runDemo).
// [Settings]
Expand All @@ -81,14 +81,20 @@ func (m *Menu) runPVP() {
pvp.Run()
}

func (m *Menu) runPVC() {
g := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypeHuman, game.PlayerTypePC)
func (m *Menu) runPVCOriginal() {
g := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypeHuman, game.PlayerTypePCOriginal)

g.Run()
}

func (m *Menu) runPVCMinMax() {
g := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypeHuman, game.PlayerTypePCMinMax)

g.Run()
}

func (m *Menu) runDemo() {
demo := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypePC, game.PlayerTypePC)
demo := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypePCOriginal, game.PlayerTypePCOriginal)
demo.Run()
}

Expand Down
28 changes: 28 additions & 0 deletions pkg/core/board/board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,34 @@ func Test_Board_IsWinner(t *testing.T) {
}
}

func Test_Board_IsWinner_TrueFalse(t *testing.T) {
tests := []struct {
name string
board *Board
letter letter.Letter
expectedResult bool
}{
{"Noone win", &Board{
width: 3,
height: 3,
chainLen: 3,
board: []letter.Letter{
0, 1, 0,
1, 0, 1,
0, 0, 1,
},
}, letter.LetterX, false},
}

for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
a := assert.New(tt)
result, _ := test.board.IsWinner(test.letter)
a.Equal(test.expectedResult, result, "unexpected result")
})
}
}

func Test_Board_GetWinner(t *testing.T) {
tests := []struct {
name string
Expand Down
89 changes: 87 additions & 2 deletions pkg/core/pcplayer/pc_player_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/rand"
"fmt"
"math/big"
"sync"

"github.com/gucio321/tic-tac-go/internal/logger"

Expand All @@ -14,19 +15,28 @@ import (
"github.com/gucio321/tic-tac-go/pkg/core/players/player"
)

type AlgType byte

const (
AlgOriginal AlgType = iota
AlgMinMax
)

var _ player.Player = &PCPlayer{}

// PCPlayer is a simple-AI logic used in Tic-Tac-Go for calculating PC-player's move.
type PCPlayer struct {
b *board.Board
pcLetter letter.Letter
algType AlgType
}

// NewPCPlayer creates new PCPlayer instance.
func NewPCPlayer(b *board.Board, pcLetter letter.Letter) *PCPlayer {
func NewPCPlayer(b *board.Board, pcLetter letter.Letter, algType AlgType) *PCPlayer {
return &PCPlayer{
b: b,
pcLetter: pcLetter,
algType: algType,
}
}

Expand All @@ -48,7 +58,82 @@ func (p *PCPlayer) String() string {
func (p PCPlayer) GetMove() (i int) {
logger.Infof("Calculating Move for PC Player")

return p.getPCMove(p.b)
switch p.algType {
case AlgOriginal:
return p.getPCMove(p.b)
case AlgMinMax:
return p.minMax(p.b)
default:
panic(fmt.Sprintf("Unknown algorithm type: %v", p.algType))
}
}

// THis is a min-max algorithm implementation.
// This algorithm predicts all possible solutions and chooses the best one.
// After writting this I found out the followint:
// 1. This is really ineffective: It is playable on 3x3 board, but on 4x4 it
// freezes my 12-core, 16GB RAM machine. (I will try to add MaxDepth (after reaching this it will just randomize the move)
// and maybe I'll try to optimize it so that it doesn't call recursively if not needed (solution worse than current worst))
// 2. This is a bit theoritical conclusion but: In theory of 3x3 tic-tac-toe game, the best 2nd move (if 1st player took corner)
// should be taking the center. However after looking at algorithnm's behaviour it turns out
// that taking the center will not lead to the fastest winning opportunity. Conclusion: the algorithm should be
// improved to consider "unblockable wins" and "draws"
func (p *PCPlayer) minMax(gameBoard *board.Board) (i int) {
cw, move, _ := p.mm(gameBoard, p.pcLetter, 0)
// now if can't get best move get random from possible
if !cw {
for i := 0; i < gameBoard.Width()*gameBoard.Height(); i++ {
if !gameBoard.IsIndexFree(i) {
continue
}
move = i
break
}
}
return move
}

func (a *PCPlayer) mm(gameBoard *board.Board, l letter.Letter, currentDepth int) (couldWin bool, move int, depth int) {
//logger.Debugf("mm: call for %s (depth: %d)\n%s", l, currentDepth, gameBoard)
depth = currentDepth
wg := sync.WaitGroup{}
m := sync.Mutex{}
for i := 0; i < gameBoard.Width()*gameBoard.Height(); i++ {
if !gameBoard.IsIndexFree(i) {
continue
}

cp := gameBoard.Copy()
cp.SetIndexState(i, l)
if winner, _ := cp.IsWinner(l); winner {
//logger.Debugf("Can win at %d (combo %v)", i, u)
return true, i, currentDepth + 1
}

if cp.IsBoardFull() {
//logger.Debugf("Cant win and board full")
return false, 0, currentDepth + 1
}

//logger.Debugf("re-running for opposite letter")
wg.Add(1)
go func() {
cpCouldWin, cpMove, cpDepth := a.mm(cp, l.Opposite(), currentDepth+1)
m.Lock()
if cpCouldWin && (cpDepth < depth || depth == currentDepth || !couldWin) {
//logger.Debugf("mm depth %d: updated best move to %d (on depth %d)", currentDepth, cpMove, cpDepth)
depth = cpDepth
move = cpMove
couldWin = true
}
m.Unlock()
wg.Done()
}()
}

wg.Wait()

return
}

//nolint:gocyclo,funlen // https://github.com/gucio321/tic-tac-go/issues/154
Expand Down
12 changes: 8 additions & 4 deletions pkg/game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,19 @@ func Create(playerXType, playerOType PlayerType) *Game {
var playerX, playerO player.Player

switch playerXType {
case PlayerTypePC:
playerX = pcplayer.NewPCPlayer(result.board, letter.LetterX)
case PlayerTypePCOriginal:
playerX = pcplayer.NewPCPlayer(result.board, letter.LetterX, pcplayer.AlgOriginal)
case PlayerTypePCMinMax:
playerX = pcplayer.NewPCPlayer(result.board, letter.LetterX, pcplayer.AlgMinMax)
case PlayerTypeHuman:
playerX = newHumanPlayer(result.getUserAction, letter.LetterX)
}

switch playerOType {
case PlayerTypePC:
playerO = pcplayer.NewPCPlayer(result.board, letter.LetterO)
case PlayerTypePCOriginal:
playerO = pcplayer.NewPCPlayer(result.board, letter.LetterO, pcplayer.AlgOriginal)
case PlayerTypePCMinMax:
playerO = pcplayer.NewPCPlayer(result.board, letter.LetterO, pcplayer.AlgMinMax)
case PlayerTypeHuman:
playerO = newHumanPlayer(result.getUserAction, letter.LetterO)
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/game/playerType.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ type PlayerType byte
// player types.
const (
PlayerTypeHuman PlayerType = iota
PlayerTypePC
PlayerTypePCOriginal
PlayerTypePCMinMax
)

0 comments on commit b94864c

Please sign in to comment.