diff --git a/cmd/giu-game/tic-tac-go.go b/cmd/giu-game/tic-tac-go.go index 580bbb8..35426f5 100644 --- a/cmd/giu-game/tic-tac-go.go +++ b/cmd/giu-game/tic-tac-go.go @@ -7,9 +7,10 @@ import ( "image/png" "log" + "github.com/gucio321/tic-tac-go/pkg/game" + "github.com/AllenDang/giu" - "github.com/gucio321/tic-tac-go/pkg/core/players/player" "github.com/gucio321/tic-tac-go/pkg/giuwidget" ) @@ -29,6 +30,6 @@ func main() { wnd := giu.NewMasterWindow("Tic-Tac-Go", screenX, screenY, 0) wnd.SetIcon([]image.Image{logo}) wnd.Run(func() { - giu.SingleWindow().Layout(giuwidget.Game(player.PlayerPerson, player.PlayerPC)) + giu.SingleWindow().Layout(giuwidget.Game(game.PlayerTypeHuman, game.PlayerTypePC)) }) } diff --git a/internal/terminalgame/game/doc.go b/internal/terminalgame/game/doc.go deleted file mode 100644 index daf24e0..0000000 --- a/internal/terminalgame/game/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package game contains terminal game implementation -package game diff --git a/internal/terminalgame/gameimpl/doc.go b/internal/terminalgame/gameimpl/doc.go new file mode 100644 index 0000000..8cfbe63 --- /dev/null +++ b/internal/terminalgame/gameimpl/doc.go @@ -0,0 +1,2 @@ +// Package gameimpl contains terminal game implementation +package gameimpl diff --git a/internal/terminalgame/game/game.go b/internal/terminalgame/gameimpl/game.go similarity index 72% rename from internal/terminalgame/game/game.go rename to internal/terminalgame/gameimpl/game.go index fef9ca4..90808ff 100644 --- a/internal/terminalgame/game/game.go +++ b/internal/terminalgame/gameimpl/game.go @@ -1,4 +1,4 @@ -package game +package gameimpl import ( "bufio" @@ -6,10 +6,11 @@ import ( "log" "os" + "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/core/players/player" "github.com/gucio321/tic-tac-go/pkg/game" ) @@ -20,10 +21,10 @@ type TTG struct { } // NewTTG creates a new TTG. -func NewTTG(w, h, chainLen byte, player1Type, player2Type player.Type) *TTG { +func NewTTG(w, h, chainLen byte, playerXType, playerOType game.PlayerType) *TTG { result := &TTG{ reader: bufio.NewReader(os.Stdin), - Game: game.Create(player1Type, player2Type), + Game: game.Create(playerXType, playerOType), } result.SetBoardSize(int(w), int(h), int(chainLen)).OnContinue(func() { @@ -41,13 +42,13 @@ func NewTTG(w, h, chainLen byte, player1Type, player2Type player.Type) *TTG { func (t *TTG) Run() { endGame := make(chan bool, 1) - t.Game.Result(func(p *player.Player) { + t.Game.Result(func(l letter.Letter, name string) { // handle game end - switch p { - case nil: + switch l { + case letter.LetterNone: fmt.Println("DRAW") default: - fmt.Println(p.Name() + " won") + fmt.Println(name + " won") } if err := menuutils.PromptEnter("Press ENTER to continue "); err != nil { diff --git a/internal/terminalgame/game/user_api.go b/internal/terminalgame/gameimpl/user_api.go similarity index 98% rename from internal/terminalgame/game/user_api.go rename to internal/terminalgame/gameimpl/user_api.go index ddcc069..6c36902 100644 --- a/internal/terminalgame/game/user_api.go +++ b/internal/terminalgame/gameimpl/user_api.go @@ -1,4 +1,4 @@ -package game +package gameimpl import ( "errors" diff --git a/internal/terminalgame/menu/menu.go b/internal/terminalgame/menu/menu.go index 21d5b49..c015376 100644 --- a/internal/terminalgame/menu/menu.go +++ b/internal/terminalgame/menu/menu.go @@ -10,19 +10,20 @@ import ( "strings" "time" + "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" - "github.com/gucio321/tic-tac-go/internal/terminalgame/game" + "github.com/gucio321/tic-tac-go/internal/terminalgame/gameimpl" "github.com/gucio321/tic-tac-go/pkg/core/board" - "github.com/gucio321/tic-tac-go/pkg/core/players/player" + "github.com/gucio321/tic-tac-go/pkg/game" ) const githubURL = "https://github.com/gucio321/tic-tac-go" @@ -83,28 +84,28 @@ func (m *Menu) Run() { } func (m *Menu) runPVP() { - pvp := game.NewTTG(m.width, m.height, m.chainLen, player.PlayerPerson, player.PlayerPerson) + pvp := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypeHuman, game.PlayerTypeHuman) pvp.Run() } func (m *Menu) runPVC() { - var g *game.TTG + var g *gameimpl.TTG // nolint:gomnd // two players in game r := rand.Intn(2) // nolint:gosec // it is ok switch r { case 0: - g = game.NewTTG(m.width, m.height, m.chainLen, player.PlayerPerson, player.PlayerPC) + g = gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypeHuman, game.PlayerTypePC) case 1: - g = game.NewTTG(m.width, m.height, m.chainLen, player.PlayerPC, player.PlayerPerson) + g = gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypePC, game.PlayerTypeHuman) } g.Run() } func (m *Menu) runDemo() { - demo := game.NewTTG(m.width, m.height, m.chainLen, player.PlayerPC, player.PlayerPC) + demo := gameimpl.NewTTG(m.width, m.height, m.chainLen, game.PlayerTypePC, game.PlayerTypePC) demo.Run() } diff --git a/pkg/core/pcplayer/pc_player_engine.go b/pkg/core/pcplayer/pc_player_engine.go index 5b63768..f85f5a8 100644 --- a/pkg/core/pcplayer/pc_player_engine.go +++ b/pkg/core/pcplayer/pc_player_engine.go @@ -8,99 +8,32 @@ import ( "github.com/gucio321/tic-tac-go/pkg/core/board" "github.com/gucio321/tic-tac-go/pkg/core/board/letter" + "github.com/gucio321/tic-tac-go/pkg/core/players/player" ) -// nolint:gochecknoinits // need to set rand seed -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func canWin(baseBoard *board.Board, player letter.Letter) (canWin bool, results []int) { - results = make([]int, 0) - - for i := 0; i < baseBoard.Width()*baseBoard.Height(); i++ { - if !baseBoard.IsIndexFree(i) { - continue - } - - fictionBoard := baseBoard.Copy() - - fictionBoard.SetIndexState(i, player) - - if ok, _ := fictionBoard.IsWinner(player); ok { - results = append(results, i) - } - } +var _ player.Player = &PCPlayer{} - return len(results) > 0, results +// 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 } -/* -This method should find situations like that: -chain length = 4 -+---+---+---+---+---+ -| | | o | | | -+---+---+---+---+---+ -| | | | | o | -+---+---+---+---+---+ -| | x | x | | | -+---+---+---+---+---+ -| | | | | | -+---+---+---+---+---+ -| | | | o | | -+---+---+---+---+---+ -let's look at the board above: when we'll make our move at right side of X-chain (14) -the O-player will not be able to keep X from winning. -+---+---+---+---+---+ -| | | o | | | -+---+---+---+---+---+ -| | | | | o | -+---+---+---+---+---+ -| | x | x | X | | -+---+---+---+---+---+ -| | | | | | -+---+---+---+---+---+ -| | | | o | | -+---+---+---+---+---+ -O-player lost. -*/ -func canWinTwoMoves(gameBoard *board.Board, player letter.Letter) (result []int) { - result = make([]int, 0) - - // nolint:gomnd // look a scheme above - in the second one, the chain is by 2 less than max - minimalChainLen := gameBoard.ChainLength() - 2 - if minimalChainLen <= 0 { - return - } - - potentiallyAvailableChains := gameBoard.GetWinBoard(gameBoard.ChainLength() + 1) - -searching: - for _, potentialPlace := range potentiallyAvailableChains { - if !gameBoard.IsIndexFree(potentialPlace[0]) || !gameBoard.IsIndexFree(potentialPlace[len(potentialPlace)-1]) { - continue - } - - var gaps []int - - for i := 1; i < len(potentialPlace)-1; i++ { - switch gameBoard.GetIndexState(potentialPlace[i]) { - case letter.LetterNone: - gaps = append(gaps, potentialPlace[i]) - case player.Opposite(): // operation already blocked - continue searching - } - } +// NewPCPlayer creates new PCPlayer instance. +func NewPCPlayer(b *board.Board, pcLetter letter.Letter) *PCPlayer { + rand.Seed(time.Now().UnixNano()) - if len(gaps) == 1 { - result = append(result, gaps...) - } + return &PCPlayer{ + b: b, + pcLetter: pcLetter, } +} - return result +func (p *PCPlayer) String() string { + return "PC " + p.pcLetter.String() } -// GetPCMove calculates move for PC player on given board. +// GetMove calculates move for PC player on given board. // Steps: // - try to win // - stop opponent from winning @@ -111,36 +44,40 @@ searching: // - take opponent's opposite corner // - take center // - take random side -// nolint:gocognit,gocyclo // https://github.com/gucio321/tic-tac-go/issues/154 -func GetPCMove(gameBoard *board.Board, pcLetter letter.Letter) (i int) { - playerLetter := pcLetter.Opposite() +func (p PCPlayer) GetMove() (i int) { + return p.getPCMove(p.b) +} + +// nolint:gocyclo // https://github.com/gucio321/tic-tac-go/issues/154 +func (p *PCPlayer) getPCMove(gameBoard *board.Board) (i int) { + playerLetter := p.pcLetter.Opposite() // attack: try to win now - if ok, indexes := canWin(gameBoard, pcLetter); ok { - options := getAvailableOptions(gameBoard, indexes) + if ok, indexes := p.canWin(gameBoard, p.pcLetter); ok { + options := p.getAvailableOptions(gameBoard, indexes) if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } } // defense: check, if user can win - if ok, indexes := canWin(gameBoard, playerLetter); ok { - options := getAvailableOptions(gameBoard, indexes) + if ok, indexes := p.canWin(gameBoard, playerLetter); ok { + options := p.getAvailableOptions(gameBoard, indexes) if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } } - options := getAvailableOptions(gameBoard, canWinTwoMoves(gameBoard, pcLetter)) + options := p.getAvailableOptions(gameBoard, p.canWinTwoMoves(gameBoard, p.pcLetter)) if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } - options = getAvailableOptions(gameBoard, canWinTwoMoves(gameBoard, playerLetter)) + options = p.getAvailableOptions(gameBoard, p.canWinTwoMoves(gameBoard, playerLetter)) if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } corners := gameBoard.GetCorners() @@ -161,7 +98,7 @@ func GetPCMove(gameBoard *board.Board, pcLetter letter.Letter) (i int) { } switch s := gameBoard.GetIndexState(i); s { - case pcLetter: + case p.pcLetter: pcOppositeCorners = append(pcOppositeCorners, o) case playerLetter: playerOppositeCorners = append(playerOppositeCorners, o) @@ -169,15 +106,15 @@ func GetPCMove(gameBoard *board.Board, pcLetter letter.Letter) (i int) { } if len(pcOppositeCorners) != 0 { - return getRandomNumber(pcOppositeCorners) + return p.getRandomNumber(pcOppositeCorners) } if len(playerOppositeCorners) != 0 { - return getRandomNumber(playerOppositeCorners) + return p.getRandomNumber(playerOppositeCorners) } if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } // try to get center @@ -188,7 +125,7 @@ func GetPCMove(gameBoard *board.Board, pcLetter letter.Letter) (i int) { } if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } for _, i := range gameBoard.GetSides() { @@ -198,25 +135,110 @@ func GetPCMove(gameBoard *board.Board, pcLetter letter.Letter) (i int) { } if len(options) > 0 { - return getRandomNumber(options) + return p.getRandomNumber(options) } const smallerBoard = 2 if newW, newH := gameBoard.Width()-smallerBoard, gameBoard.Height()-smallerBoard; newW > 0 && newH > 0 { - return gameBoard.ConvertIndex(newW, newH, GetPCMove(gameBoard.Cut(newW, newH), pcLetter)) + return gameBoard.ConvertIndex(newW, newH, p.getPCMove(gameBoard.Cut(newW, newH))) } panic("Tic-Tac-Go: pcplayer.GetPCMove(...): cannot determinate pc move - board is full") } -func getRandomNumber(numbers []int) int { +func (p *PCPlayer) canWin(baseBoard *board.Board, playerLetter letter.Letter) (canWin bool, results []int) { + results = make([]int, 0) + + for i := 0; i < baseBoard.Width()*baseBoard.Height(); i++ { + if !baseBoard.IsIndexFree(i) { + continue + } + + fictionBoard := baseBoard.Copy() + + fictionBoard.SetIndexState(i, playerLetter) + + if ok, _ := fictionBoard.IsWinner(playerLetter); ok { + results = append(results, i) + } + } + + return len(results) > 0, results +} + +/* +This method should find situations like that: +chain length = 4 ++---+---+---+---+---+ +| | | o | | | ++---+---+---+---+---+ +| | | | | o | ++---+---+---+---+---+ +| | x | x | | | ++---+---+---+---+---+ +| | | | | | ++---+---+---+---+---+ +| | | | o | | ++---+---+---+---+---+ +let's look at the board above: when we'll make our move at right side of X-chain (14) +the O-player will not be able to keep X from winning. ++---+---+---+---+---+ +| | | o | | | ++---+---+---+---+---+ +| | | | | o | ++---+---+---+---+---+ +| | x | x | X | | ++---+---+---+---+---+ +| | | | | | ++---+---+---+---+---+ +| | | | o | | ++---+---+---+---+---+ +O-player lost. +*/ +func (p *PCPlayer) canWinTwoMoves(gameBoard *board.Board, playerLetter letter.Letter) (result []int) { + result = make([]int, 0) + + // nolint:gomnd // look a scheme above - in the second one, the chain is by 2 less than max + minimalChainLen := gameBoard.ChainLength() - 2 + if minimalChainLen <= 0 { + return + } + + potentiallyAvailableChains := gameBoard.GetWinBoard(gameBoard.ChainLength() + 1) + +searching: + for _, potentialPlace := range potentiallyAvailableChains { + if !gameBoard.IsIndexFree(potentialPlace[0]) || !gameBoard.IsIndexFree(potentialPlace[len(potentialPlace)-1]) { + continue + } + + var gaps []int + + for i := 1; i < len(potentialPlace)-1; i++ { + switch gameBoard.GetIndexState(potentialPlace[i]) { + case letter.LetterNone: + gaps = append(gaps, potentialPlace[i]) + case playerLetter.Opposite(): // operation already blocked + continue searching + } + } + + if len(gaps) == 1 { + result = append(result, gaps...) + } + } + + return result +} + +func (p *PCPlayer) getRandomNumber(numbers []int) int { // nolint:gosec // it's ok result := numbers[rand.Intn(len(numbers))] return result } -func getAvailableOptions(gameBoard *board.Board, candidates []int) (available []int) { +func (p *PCPlayer) getAvailableOptions(gameBoard *board.Board, candidates []int) (available []int) { available = make([]int, 0) for _, i := range candidates { diff --git a/pkg/core/pcplayer/pc_player_engine_test.go b/pkg/core/pcplayer/pc_player_engine_test.go index 3694fa9..f20185c 100644 --- a/pkg/core/pcplayer/pc_player_engine_test.go +++ b/pkg/core/pcplayer/pc_player_engine_test.go @@ -247,7 +247,7 @@ func TestGetPCMove(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := GetPCMove(tt.args.gameBoard, tt.args.pcLetter) + got := NewPCPlayer(tt.args.gameBoard, tt.args.pcLetter).GetMove() assert.Truef(t, contains(tt.want, got), "GetPCMove() returned unexpected result: expected %v, got %v", tt.want, got) }) } @@ -272,8 +272,8 @@ func TestGetPCMove_FullBoard(t *testing.T) { SetIndexState(14, letter.LetterX). SetIndexState(15, letter.LetterO) - assert.Panics(t, func() { GetPCMove(gameBoard, letter.LetterX) }, "GetPCMove on full board didn't panicked") - assert.Panics(t, func() { GetPCMove(gameBoard, letter.LetterO) }, "GetPCMove on full board didn't panicked") + assert.Panics(t, func() { NewPCPlayer(gameBoard, letter.LetterX).GetMove() }, "GetPCMove on full board didn't panicked") + assert.Panics(t, func() { NewPCPlayer(gameBoard, letter.LetterO).GetMove() }, "GetPCMove on full board didn't panicked") } //nolint:funlen // tests function; it is ok @@ -430,8 +430,8 @@ func Test_canWin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotCanWinX, gotResultsX := canWin(tt.args.baseBoard, letter.LetterX) - gotCanWinO, gotResultsO := canWin(tt.args.baseBoard, letter.LetterO) + gotCanWinX, gotResultsX := NewPCPlayer(tt.args.baseBoard, letter.LetterX).canWin(tt.args.baseBoard, letter.LetterX) + gotCanWinO, gotResultsO := NewPCPlayer(tt.args.baseBoard, letter.LetterO).canWin(tt.args.baseBoard, letter.LetterO) assert.Equal(t, gotCanWinX, tt.wantCanWinX, "canWin returned unexpected value") assert.Equal(t, gotResultsX, tt.wantResultsX, "canWin returned unexpected value (list of winning combos)") assert.Equal(t, gotCanWinO, tt.wantCanWinO, "canWin returned unexpected value") @@ -504,7 +504,7 @@ func Test_canWinTwoMoves(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotResult := canWinTwoMoves(tt.args.gameBoard, tt.args.player) + gotResult := NewPCPlayer(tt.args.gameBoard, tt.args.player).canWinTwoMoves(tt.args.gameBoard, tt.args.player) assert.Equal(t, tt.wantResult, gotResult, "canWinTwoMoves() = %v, want %v", gotResult, tt.wantResult) }) } diff --git a/pkg/core/players/player/player.go b/pkg/core/players/player/player.go index 0d61ffa..123444d 100644 --- a/pkg/core/players/player/player.go +++ b/pkg/core/players/player/player.go @@ -2,75 +2,10 @@ package player import ( "fmt" - - "github.com/gucio321/tic-tac-go/pkg/core/board/letter" -) - -// Callback is a player move callback. -type Callback func(letter.Letter) int - -// Type represents players' types. -type Type int - -// player types. -const ( - PlayerPC Type = iota - PlayerPerson ) -func (p Type) String() string { - lookup := map[Type]string{ - PlayerPC: "PC", - PlayerPerson: "Player", - } - - result, ok := lookup[p] - - if !ok { - panic(fmt.Sprintf("Tic-Tac-Go: player.Type.String(): unknown player type %d", p)) - } - - return result -} - -// Player represents the game player. -type Player struct { - playerType Type - letter letter.Letter - moveCb Callback -} - -// Create creates a new player. -func Create(t Type, playerLetter letter.Letter, cb Callback) *Player { - result := &Player{ - playerType: t, - letter: playerLetter, - moveCb: cb, - } - - return result -} - -// Move 'makes' player's move. -func (p *Player) Move() int { - if p.moveCb == nil { - panic("player.(*Player).Move(): moveCb cannot be nil!") - } - - return p.moveCb(p.Letter()) -} - -// Letter returns player's letter. -func (p *Player) Letter() letter.Letter { - return p.letter -} - -// Name returns player's name. -func (p *Player) Name() string { - return fmt.Sprintf("%s %s", p.playerType, p.letter) -} - -// Type returns player's type. -func (p *Player) Type() Type { - return p.playerType +// Player is an interface implemented by any further Player implementations. +type Player interface { + GetMove() int + fmt.Stringer } diff --git a/pkg/core/players/player/player_test.go b/pkg/core/players/player/player_test.go deleted file mode 100644 index a854678..0000000 --- a/pkg/core/players/player/player_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package player - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/gucio321/tic-tac-go/pkg/core/board/letter" -) - -func Test_PlayerType_String_invalid_type(t *testing.T) { - assert.Panics(t, func() { _ = Type(5).String() }, "Calling string method of inocorrecty player's type didn't panicked") -} - -func Test_Create(t *testing.T) { - const num = 8 - - a := assert.New(t) - player := Create(PlayerPerson, letter.LetterX, func(_ letter.Letter) int { return num }) - - a.Equal(PlayerPerson, player.playerType, "Unexpected player created") - a.Equal(letter.LetterX, player.letter, "Unexpected player created") - a.Equal(num, player.moveCb(player.letter), "Unexpected player created") -} - -func Test_Move(t *testing.T) { - tests := []struct { - name string - number int - }{ - {"Standard test", 8}, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - player := Create(PlayerPerson, letter.LetterX, func(_ letter.Letter) int { return test.number }) - - assert.Equal(tt, test.number, player.Move(), "unexpected move done") - }) - } -} - -func Test_Move_nil_callback(t *testing.T) { - assert.Panics(t, func() { - player := Create(PlayerPerson, letter.LetterX, nil) - player.Move() - }, "calling player.Move with nil callback didn't panicked") -} - -func Test_Letter(t *testing.T) { - player := Create(PlayerPerson, letter.LetterX, nil) - - assert.Equal(t, letter.LetterX, player.Letter(), "unexpected letter returned") -} - -func Test_Name(t *testing.T) { - tests := []struct { - name string - expected string - pt Type - pl letter.Letter - }{ - {"person x", "Player X", PlayerPerson, letter.LetterX}, - {"pc o", "PC O", PlayerPC, letter.LetterO}, - } - - for _, test := range tests { - t.Run(test.name, func(tt *testing.T) { - player := Create(test.pt, test.pl, nil) - - assert.Equal(tt, test.expected, player.Name(), "unexpected name returned") - }) - } -} - -func Test_Type(t *testing.T) { - player := Create(PlayerPerson, letter.LetterX, nil) - - assert.Equal(t, PlayerPerson, player.Type(), "Unexpected type of the player returned") -} diff --git a/pkg/core/players/players.go b/pkg/core/players/players.go index dfddaaa..4f03d97 100644 --- a/pkg/core/players/players.go +++ b/pkg/core/players/players.go @@ -9,48 +9,53 @@ import ( // Players represents a pair of players. type Players struct { - player1, - player2 *player.Player + playerX, + playerO player.Player current letter.Letter } // Create creates a new players set. -func Create(player1Type player.Type, cb1 player.Callback, player2Type player.Type, cb2 player.Callback) *Players { +func Create(playerX, playerO player.Player) *Players { result := &Players{ - player1: player.Create(player1Type, letter.LetterX, cb1), - player2: player.Create(player2Type, letter.LetterO, cb2), + playerO: playerX, + playerX: playerO, current: letter.LetterX, } return result } -// Player1 returns player1. -func (p *Players) Player1() *player.Player { - return p.player1 +// PlayerX returns X player. +func (p *Players) PlayerX() player.Player { + return p.playerX } -// Player2 returns player2. -func (p *Players) Player2() *player.Player { - return p.player2 +// PlayerO returns O player. +func (p *Players) PlayerO() player.Player { + return p.playerO } -// Current returns current player. -func (p *Players) Current() *player.Player { +// Current returns current player's letter. +func (p *Players) Current() letter.Letter { + return p.current +} + +// CurrentPlayer returns current player. +func (p *Players) CurrentPlayer() player.Player { switch p.current { - case p.player1.Letter(): - return p.player1 - case p.player2.Letter(): - return p.player2 + case letter.LetterX: + return p.playerX + case letter.LetterO: + return p.playerO } return nil } -// Move returns a current player's move. -func (p *Players) Move() int { - return p.Current().Move() +// GetMove returns a current player's move. +func (p *Players) GetMove() int { + return p.CurrentPlayer().GetMove() } // Next switch to the next player. diff --git a/pkg/game/game.go b/pkg/game/game.go index 1f871a8..1b2a51f 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -32,39 +32,39 @@ type Game struct { isRunning bool onContinue func() - resultCB func(*player.Player) + resultCB func(winnerLetter letter.Letter, winnerName string) userActionCB func() int } // Create creates a game instance. -func Create(p1type, p2type player.Type) *Game { +func Create(playerXType, playerOType PlayerType) *Game { result := &Game{ isRunning: false, board: board.Create(defaultBoardW, defaultBoardH, defaultChainLen), onContinue: func() {}, - resultCB: func(*player.Player) {}, + resultCB: func(letter.Letter, string) {}, userActionCB: func() int { panic("Tic-Tac-Go: game.(*Game): user action callback is not set!") }, } - var p1Cb, p2Cb func(letter.Letter) int + var playerX, playerO player.Player - switch p1type { - case player.PlayerPC: - p1Cb = func(l letter.Letter) int { return pcplayer.GetPCMove(result.board, l) } - case player.PlayerPerson: - p1Cb = func(_ letter.Letter) int { return result.getUserAction() } + switch playerXType { + case PlayerTypePC: + playerX = pcplayer.NewPCPlayer(result.board, letter.LetterX) + case PlayerTypeHuman: + playerX = newHumanPlayer(result.getUserAction, letter.LetterX) } - switch p2type { - case player.PlayerPC: - p2Cb = func(l letter.Letter) int { return pcplayer.GetPCMove(result.board, l) } - case player.PlayerPerson: - p2Cb = func(_ letter.Letter) int { return result.getUserAction() } + switch playerOType { + case PlayerTypePC: + playerO = pcplayer.NewPCPlayer(result.board, letter.LetterO) + case PlayerTypeHuman: + playerO = newHumanPlayer(result.getUserAction, letter.LetterO) } - result.players = players.Create(p1type, p1Cb, p2type, p2Cb) + result.players = players.Create(playerX, playerO) return result } @@ -75,7 +75,7 @@ func Create(p1type, p2type player.Type) *Game { func (g *Game) SetBoardSize(w, h, c int) *Game { g.isRunningPanic("SetBoardSize") - g.board = board.Create(w, h, c) + *g.board = *board.Create(w, h, c) return g } @@ -101,7 +101,7 @@ func (g *Game) UserAction(cb func() int) { // Result returns true if game is ended. in addition, it returns its result. // if LetterNone returned - it means that DRAW reached. -func (g *Game) Result(resultCB func(*player.Player)) *Game { +func (g *Game) Result(resultCB func(winnerLetter letter.Letter, winnerName string)) *Game { g.resultCB = resultCB return g @@ -118,10 +118,10 @@ func (g *Game) Board() *board.Board { } // CurrentPlayer returns a current player. -func (g *Game) CurrentPlayer() *player.Player { +func (g *Game) CurrentPlayer() player.Player { g.notRunningPanic("CurrentPlayer") - return g.players.Current() + return g.players.CurrentPlayer() } // Run runs the game. @@ -138,25 +138,25 @@ func (g *Game) Run() { // main loop for { g.onContinue() - idx := g.players.Current().Move() + idx := g.players.CurrentPlayer().GetMove() // if loop was stopped by Dispose() or Stop(), exit the loop if !g.isRunning { return } - g.Board().SetIndexState(idx, g.players.Current().Letter()) + g.Board().SetIndexState(idx, g.players.Current()) - if ok, _ := g.Board().IsWinner(g.players.Current().Letter()); ok { + if ok, _ := g.Board().IsWinner(g.players.Current()); ok { g.onContinue() g.isRunning = false - g.resultCB(g.players.Current()) + g.resultCB(g.players.Current(), g.players.CurrentPlayer().String()) return } else if g.Board().IsBoardFull() { g.onContinue() g.isRunning = false - g.resultCB(nil) + g.resultCB(letter.LetterNone, "") return } @@ -179,24 +179,6 @@ func (g *Game) Reset() { } *g.board = *board.Create(g.board.Width(), g.board.Height(), g.board.ChainLength()) - - var p1Cb, p2Cb func(letter.Letter) int - - switch g.players.Player1().Type() { - case player.PlayerPC: - p1Cb = func(l letter.Letter) int { return pcplayer.GetPCMove(g.board, l) } - case player.PlayerPerson: - p1Cb = func(_ letter.Letter) int { return g.getUserAction() } - } - - switch g.players.Player2().Type() { - case player.PlayerPC: - p2Cb = func(l letter.Letter) int { return pcplayer.GetPCMove(g.board, l) } - case player.PlayerPerson: - p2Cb = func(_ letter.Letter) int { return g.getUserAction() } - } - - g.players = players.Create(g.players.Player1().Type(), p1Cb, g.players.Player2().Type(), p2Cb) } // Stop safely stops the game loop invoked by (*Game).Run. diff --git a/pkg/game/player.go b/pkg/game/player.go new file mode 100644 index 0000000..7f382e5 --- /dev/null +++ b/pkg/game/player.go @@ -0,0 +1,28 @@ +package game + +import ( + "github.com/gucio321/tic-tac-go/pkg/core/board/letter" + "github.com/gucio321/tic-tac-go/pkg/core/players/player" +) + +var _ player.Player = &humanPlayer{} + +type humanPlayer struct { + callback func() int + letter letter.Letter +} + +func newHumanPlayer(cb func() int, l letter.Letter) *humanPlayer { + return &humanPlayer{ + callback: cb, + letter: l, + } +} + +func (h *humanPlayer) GetMove() int { + return h.callback() +} + +func (h *humanPlayer) String() string { + return "Player " + h.letter.String() +} diff --git a/pkg/game/playerType.go b/pkg/game/playerType.go new file mode 100644 index 0000000..fd68934 --- /dev/null +++ b/pkg/game/playerType.go @@ -0,0 +1,10 @@ +package game + +// PlayerType represents type of player (human or Computer). +type PlayerType byte + +// player types. +const ( + PlayerTypeHuman PlayerType = iota + PlayerTypePC +) diff --git a/pkg/giuwidget/state.go b/pkg/giuwidget/state.go index 83beecd..3374295 100644 --- a/pkg/giuwidget/state.go +++ b/pkg/giuwidget/state.go @@ -3,8 +3,9 @@ package giuwidget import ( "github.com/AllenDang/giu" + "github.com/gucio321/tic-tac-go/pkg/core/board/letter" + "github.com/gucio321/tic-tac-go/pkg/core/board" - "github.com/gucio321/tic-tac-go/pkg/core/players/player" "github.com/gucio321/tic-tac-go/pkg/game" ) @@ -52,11 +53,11 @@ func (g *GameWidget) newState() *gameState { h: defaultBoardSize, chainLen: defaultBoardSize, gameEnded: false, - game: game.Create(g.p1type, g.p2type), + game: game.Create(g.playerXType, g.playerOType), buttonClick: make(chan int), } - state.game.Result(func(p *player.Player) { + state.game.Result(func(letter.Letter, string) { _, state.winningCombo = state.currentBoard.GetWinner() state.gameEnded = true }) diff --git a/pkg/giuwidget/widget.go b/pkg/giuwidget/widget.go index bd84af7..2850710 100644 --- a/pkg/giuwidget/widget.go +++ b/pkg/giuwidget/widget.go @@ -10,7 +10,7 @@ import ( "golang.org/x/image/colornames" "github.com/gucio321/tic-tac-go/pkg/core/board/letter" - "github.com/gucio321/tic-tac-go/pkg/core/players/player" + "github.com/gucio321/tic-tac-go/pkg/game" ) const id = "Tic-Tac-Go-game" @@ -25,14 +25,14 @@ const ( // GameWidget represents a giu implementation of tic-tac-go. type GameWidget struct { - p1type, p2type player.Type + playerXType, playerOType game.PlayerType } // Game creates GameWidget. -func Game(p1type, p2type player.Type) *GameWidget { +func Game(playerXType, playerOType game.PlayerType) *GameWidget { return &GameWidget{ - p1type: p1type, - p2type: p2type, + playerXType: playerXType, + playerOType: playerOType, } }