Permalink
Browse files

Add human-controlled player.

  • Loading branch information...
dmitshur committed Oct 10, 2017
1 parent ea37f12 commit 2233059014b0c53f1f7a1bf2b09019f5ecf5961c
Showing with 99 additions and 23 deletions.
  1. +6 −5 README.md
  2. +19 −8 cmd/tictactoe/display.go
  3. +30 −10 cmd/tictactoe/game.go
  4. +36 −0 player/human/human.go
  5. +8 −0 tictactoe.go
View
@@ -15,11 +15,12 @@ go get -u github.com/shurcooL/tictactoe
Directories
-----------
| Path | Synopsis |
|--------------------------------------------------------------------------------|-----------------------------------------------------------|
| [cmd/tictactoe](https://godoc.org/github.com/shurcooL/tictactoe/cmd/tictactoe) | tictactoe plays a game of tic-tac-toe with two players. |
| [player/bad](https://godoc.org/github.com/shurcooL/tictactoe/player/bad) | Package bad contains a bad tic-tac-toe player. |
| [player/random](https://godoc.org/github.com/shurcooL/tictactoe/player/random) | Package random implements a random player of tic-tac-toe. |
| Path | Synopsis |
|--------------------------------------------------------------------------------|---------------------------------------------------------------|
| [cmd/tictactoe](https://godoc.org/github.com/shurcooL/tictactoe/cmd/tictactoe) | tictactoe plays a game of tic-tac-toe with two players. |
| [player/bad](https://godoc.org/github.com/shurcooL/tictactoe/player/bad) | Package bad contains a bad tic-tac-toe player. |
| [player/human](https://godoc.org/github.com/shurcooL/tictactoe/player/human) | Package human contains a human-controlled tic-tac-toe player. |
| [player/random](https://godoc.org/github.com/shurcooL/tictactoe/player/random) | Package random implements a random player of tic-tac-toe. |
License
-------
View
@@ -73,9 +73,9 @@ func (b board) Render() []*html.Node {
table := &html.Node{Data: atom.Table.String(), Type: html.ElementNode}
for row := 0; row < 3; row++ {
tr := &html.Node{Data: atom.Tr.String(), Type: html.ElementNode}
for _, cell := range b.Cells[3*row : 3*row+3] {
for col, cell := range b.Cells[3*row : 3*row+3] {
td := &html.Node{Data: atom.Td.String(), Type: html.ElementNode}
htmlg.AppendChildren(td, boardCell{cell}.Render()...)
htmlg.AppendChildren(td, boardCell{State: cell, Index: 3*row + col}.Render()...)
tr.AppendChild(td)
}
table.AppendChild(tr)
@@ -86,15 +86,26 @@ func (b board) Render() []*html.Node {
}
// boardCell renders a board cell.
type boardCell struct{ ttt.State }
type boardCell struct {
ttt.State
Index int
}
func (c boardCell) Render() []*html.Node {
return []*html.Node{style(
`display: table-cell; width: 30px; height: 30px; text-align: center; vertical-align: middle; background-color: #f4f4f4;`,
htmlg.Div(
htmlg.Text(c.String()),
cell := &html.Node{
Type: html.ElementNode, Data: atom.A.String(),
Attr: []html.Attribute{
{Key: atom.Style.String(), Val: "cursor: pointer;"},
{Key: atom.Onclick.String(), Val: fmt.Sprintf("CellClick(%d);", c.Index)},
},
FirstChild: style(
`display: table-cell; width: 30px; height: 30px; text-align: center; vertical-align: middle; background-color: #f4f4f4;`,
htmlg.Div(
htmlg.Text(c.String()),
),
),
)}
}
return []*html.Node{cell}
}
// Render the player.
View
@@ -6,6 +6,7 @@ import (
"runtime"
"time"
"github.com/gopherjs/gopherjs/js"
"github.com/shurcooL/htmlg"
ttt "github.com/shurcooL/tictactoe"
"honnef.co/go/js/dom"
@@ -16,6 +17,19 @@ import (
func playGame(players [2]player) (ttt.Condition, error) {
var board ttt.Board // Start with an empty board.
// When a board cell is clicked, its [0, 9) index is sent to this channel.
var cellClick chan int
if runtime.GOARCH == "js" {
cellClick = make(chan int)
js.Global.Set("CellClick", func(index int) {
select {
case cellClick <- index:
default:
}
})
}
fmt.Println()
fmt.Println(board)
if runtime.GOARCH == "js" {
@@ -24,7 +38,7 @@ func playGame(players [2]player) (ttt.Condition, error) {
}
for i := 0; ; i++ {
err := playerTurn(&board, players[i%2])
err := playerTurn(&board, players[i%2], cellClick)
if err != nil {
if runtime.GOARCH == "js" {
var document = dom.GetWindow().Document().(dom.HTMLDocument)
@@ -48,12 +62,12 @@ func playGame(players [2]player) (ttt.Condition, error) {
}
// playerTurn gets the player p's move and applies it to board b.
func playerTurn(b *ttt.Board, player player) error {
func playerTurn(b *ttt.Board, player player, cellClick <-chan int) error {
const timePerTurn = 3 * time.Second
started := time.Now()
move, err := playerMove(*b, player, timePerTurn)
move, err := playerMove(*b, player, timePerTurn, cellClick)
if err != nil {
return fmt.Errorf("player %v (%s) failed to make a move: %v", player.Mark, player.Name(), err)
}
@@ -66,13 +80,14 @@ func playerTurn(b *ttt.Board, player player) error {
return fmt.Errorf("player %v (%s) made a move that isn't legal: %v", player.Mark, player.Name(), err)
}
// Enforce a minimum of 1 second per move.
time.Sleep(time.Second - time.Since(started))
return nil
}
// playerMove gets the player p's move, enforcing the timeout.
func playerMove(b ttt.Board, p player, timeout time.Duration) (ttt.Move, error) {
func playerMove(b ttt.Board, p player, timeout time.Duration, cellClick <-chan int) (ttt.Move, error) {
type moveError struct {
ttt.Move
err error
@@ -95,11 +110,16 @@ func playerMove(b ttt.Board, p player, timeout time.Duration) (ttt.Move, error)
resultCh <- moveError{move, err}
}()
var result moveError
select {
case result = <-resultCh:
return result.Move, result.err
case <-ctx.Done():
return 0, fmt.Errorf("took more than allotted time of %v", timeout)
for {
select {
case result := <-resultCh:
return result.Move, result.err
case index := <-cellClick:
if p, ok := p.Player.(ttt.CellClicker); ok {
p.CellClick(index)
}
case <-ctx.Done():
return 0, fmt.Errorf("took more than allotted time of %v", timeout)
}
}
}
View
@@ -0,0 +1,36 @@
// Package human contains a human-controlled tic-tac-toe player.
package human
import (
"context"
"github.com/shurcooL/tictactoe"
)
// NewPlayer creates a human-controlled player.
func NewPlayer() (tictactoe.Player, error) {
return player{chosenMove: make(chan tictactoe.Move)}, nil
}
type player struct {
chosenMove chan tictactoe.Move
}
// Name of player.
func (player) Name() string {
return "Human Player"
}
// Play takes a tic-tac-toe board b and returns the next move
// for this player. Its mark is either X or O.
// ctx is expected to have a deadline set, and Play may take time
// to "think" until deadline is reached before returning.
func (p player) Play(ctx context.Context, b tictactoe.Board, mark tictactoe.State) (tictactoe.Move, error) {
// Outsource our decision-making process to the human.
// They know what they're doing. Hopefully.
return <-p.chosenMove, nil
}
func (p player) CellClick(index int) {
p.chosenMove <- tictactoe.Move(index)
}
View
@@ -28,6 +28,14 @@ type Imager interface {
Image() template.URL
}
// CellClicker is an optional interface implemented by players
// that wish to be notified about cell clicks.
type CellClicker interface {
// CellClick is called when cell with
// specified index is clicked.
CellClick(index int)
}
// Move is the board cell index where to place one's mark, a value in range [0, 9).
type Move int

0 comments on commit 2233059

Please sign in to comment.