Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async player draft #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions pazaakcli/cmd/humancli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/Songmu/prompter"
"github.com/loopfz/pazaak/pazaakcli/pazaak"
)

func main() {

resp, err := http.Get("http://localhost:8087/state")
if err != nil {
panic(err)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}

resp.Body.Close()

game := pazaak.PazaakGame{}

err = json.Unmarshal(body, &game)
if err != nil {
panic(err)
}

var handIdent []string
for _, c := range game.CurrentPlayer.Hand {
handIdent = append(handIdent, c.Identifier)
}

fmt.Println("----------")
fmt.Println("OPPONENT")
fmt.Println("----------")
fmt.Printf("Round wins: %d\n", game.Opponent.RoundWins)
fmt.Printf("Board value: %d\n", game.Opponent.BoardValue)
fmt.Printf("Stands: %v\n", game.Opponent.Stand)

fmt.Println("----------")
fmt.Println("YOU")
fmt.Println("----------")
fmt.Printf("Round wins: %d\n", game.CurrentPlayer.RoundWins)
fmt.Printf("Board value: %d\n", game.CurrentPlayer.BoardValue)
fmt.Printf("Hand: %s\n", strings.Join(handIdent, ", "))

move := &pazaak.PazaakMove{}

move.HandCard = prompter.Prompt("Play hand card?", "")
if strings.HasPrefix(move.HandCard, "+-") {
move.FlipCard = prompter.YN("Flip card (use negative value) ?", false)
}
move.Stand = prompter.YN("Stand?", false)

movestr, err := json.Marshal(move)
if err != nil {
panic(err)
}

_, err = http.Post("http://localhost:8087/move", "application/json", bytes.NewBuffer(movestr))
if err != nil {
panic(err)
}

}
58 changes: 58 additions & 0 deletions pazaakcli/cmd/multipaz/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"flag"
"fmt"
"os"

"github.com/gin-gonic/gin"
"github.com/loopfz/pazaak/pazaakcli/pazaak"
"github.com/loopfz/pazaak/pazaakcli/player"
"github.com/sirupsen/logrus"
)

var human *player.AsyncPlayer

func main() {

bot := flag.String("player", "", "player program path")
quiet := flag.Bool("quiet", false, "no logs")
flag.Parse()

if *quiet {
logrus.SetLevel(logrus.ErrorLevel)
}

if *bot == "" {
panic("missing bot")
}

aiPlayer := player.NewForkPlayer(*bot)
human = player.NewAsyncPlayer()

router := gin.Default()
router.GET("/state", getState)
router.POST("/move", doMove)

g, err := pazaak.NewGame([]player.Player{aiPlayer, human}, "", pazaak.AutoSidedeckHandler{})
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] %s\n", err)
os.Exit(1)
}

go func() { router.Run(":8087") }()

g.Run()
}

func getState(c *gin.Context) {
human.GetState(c.Writer)
}

func doMove(c *gin.Context) {

err := human.DoMove(c.Request)
if err != nil {
c.AbortWithStatusJSON(400, map[string]string{"error": err.Error()})
}
}
4 changes: 2 additions & 2 deletions pazaakcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ func main() {
logrus.SetLevel(logrus.ErrorLevel)
}

var pl []*player.Player
var pl []player.Player
for _, p := range playerList {
pl = append(pl, player.NewForkPlayer(p))
}

g, err := pazaak.NewGame(pl, *statsFile)
g, err := pazaak.NewGame(pl, *statsFile, pazaak.StdinSidedeckHandler{})
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] %s\n", err)
os.Exit(1)
Expand Down
41 changes: 30 additions & 11 deletions pazaakcli/pazaak/pazaak.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ type PazaakGame struct {
}

type PazaakPlayer struct {
*player.Player
player.Player `json:"-"`

Number int `json:"number"`

// Not reset between rounds
SideDeck []*PazaakCard `json:"-"`
Expand Down Expand Up @@ -75,6 +77,25 @@ type Stats struct {
Score map[string]int `json:"score"`
}

type SidedeckHandler interface {
GetDecks() [2]string
}

type StdinSidedeckHandler struct{}

func (s StdinSidedeckHandler) GetDecks() [2]string {
reader := bufio.NewReader(os.Stdin)
s1, _ := reader.ReadString('\n')
s2, _ := reader.ReadString('\n')
return [2]string{strings.TrimSpace(s1), strings.TrimSpace(s2)}
}

type AutoSidedeckHandler struct{}

func (a AutoSidedeckHandler) GetDecks() [2]string {
return [2]string{"auto", "auto"}
}

func init() {
rand.Seed(time.Now().UnixNano())
}
Expand All @@ -83,20 +104,19 @@ func (m *PazaakMove) Valid() error {
return nil
}

func NewGame(pl []*player.Player, statsFile string) (*PazaakGame, error) {
func NewGame(pl []player.Player, statsFile string, sdh SidedeckHandler) (*PazaakGame, error) {

g := &PazaakGame{StatsFile: statsFile}

for _, p := range pl {
p.Number = uint(len(g.Players) + 1)
g.Players = append(g.Players, &PazaakPlayer{Player: p})
g.Players = append(g.Players, &PazaakPlayer{Player: p, Number: len(g.Players) + 1})
}

if len(g.Players) != 2 {
return nil, errors.New("Player count should be 2")
}

err := g.InitPlayerSideDecks()
err := g.InitPlayerSideDecks(sdh)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -231,11 +251,10 @@ func buildRandomSideDeck(includeSimple, includeFlip bool) []string {
return ret
}

func (g *PazaakGame) InitPlayerSideDecks() error {
reader := bufio.NewReader(os.Stdin)
for _, p := range g.Players {
s, _ := reader.ReadString('\n')
s = strings.TrimSpace(s)
func (g *PazaakGame) InitPlayerSideDecks(sdh SidedeckHandler) error {
decks := sdh.GetDecks()
for i, p := range g.Players {
s := strings.TrimSpace(decks[i])
var cards []string
switch s {
case AUTO_SIDEDECK:
Expand Down Expand Up @@ -466,5 +485,5 @@ func (p PazaakPlayer) String() string {
for _, c := range p.Hand {
hand = append(hand, c.Identifier)
}
return fmt.Sprintf("%s [%d] {%v}", p.Player, p.BoardValue, hand)
return fmt.Sprintf("%d (%s) [%d] {%v}", p.Number, p.Player, p.BoardValue, hand)
}
103 changes: 93 additions & 10 deletions pazaakcli/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"sync"
"time"
)

const (
PLAYER_TIMEOUT = 5 * time.Second

ASYNC_TIMEOUT = 5 * time.Minute
)

type Player struct {
type Player interface {
GetMove(GameEngine) (PlayerMove, error)
}

type AIPlayer struct {
Program string `json:"-"`
Number uint `json:"number"`
// Data *json.RawMessage `json:"data"` // TODO
executor func(*Player, GameEngine) (PlayerMove, error) `json:"-"`
executor func(*AIPlayer, GameEngine) (PlayerMove, error) `json:"-"`
}

type GameEngine interface {
Expand All @@ -29,25 +37,25 @@ type PlayerMove interface {
Valid() error
}

func NewForkPlayer(bin string) *Player {
return &Player{
func NewForkPlayer(bin string) Player {
return &AIPlayer{
Program: bin,
executor: ForkMove,
}
}

func (p *Player) String() string {
return fmt.Sprintf("%d (%s)", p.Number, p.Program)
func (p *AIPlayer) String() string {
return p.Program
}

func (p *Player) GetMove(g GameEngine) (PlayerMove, error) {
func (p *AIPlayer) GetMove(g GameEngine) (PlayerMove, error) {
if p.executor == nil {
return nil, errors.New("No executor set")
}
return p.executor(p, g)
}

func ForkMove(p *Player, g GameEngine) (PlayerMove, error) {
func ForkMove(p *AIPlayer, g GameEngine) (PlayerMove, error) {

cmd := exec.Command(p.Program)

Expand Down Expand Up @@ -75,7 +83,7 @@ func ForkMove(p *Player, g GameEngine) (PlayerMove, error) {
select {
case <-time.After(PLAYER_TIMEOUT):
if err := cmd.Process.Kill(); err != nil {
fmt.Fprintf(os.Stderr, "Player %s (%d): Failed to kill subprocess after timeout: %s", p.Number, p.Program, err)
fmt.Fprintf(os.Stderr, "Player %s: Failed to kill subprocess after timeout: %s", p.Program, err)
}
<-done
return nil, errors.New("Timeout")
Expand All @@ -98,3 +106,78 @@ func ForkMove(p *Player, g GameEngine) (PlayerMove, error) {

return move, nil
}

type AsyncPlayer struct {
mut sync.Mutex
gameState []byte
deadline time.Time
g GameEngine
respChan chan PlayerMove
}

func NewAsyncPlayer() *AsyncPlayer {
return &AsyncPlayer{
gameState: []byte(`{}`),
respChan: make(chan PlayerMove),
}
}

func (ap *AsyncPlayer) GetMove(g GameEngine) (PlayerMove, error) {

ap.mut.Lock()
rawG, err := json.Marshal(g)
if err != nil {
return nil, err
}
ap.gameState = rawG
ap.g = g
deadline := time.Now().Add(ASYNC_TIMEOUT)
ap.deadline = deadline
ch := ap.respChan
ap.mut.Unlock()
select {
case resp := <-ch:
return resp, nil
case <-time.After(ASYNC_TIMEOUT):
}

return nil, errors.New("human timeout")
}

func (ap *AsyncPlayer) GetState(w http.ResponseWriter) {
ap.mut.Lock()
w.Write(ap.gameState)
ap.mut.Unlock()
}

func (ap *AsyncPlayer) DoMove(r *http.Request) error {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
ap.mut.Lock()
defer ap.mut.Unlock()
if ap.g == nil {
return errors.New("game has not started!")
}
move := ap.g.NewMove()
err = json.Unmarshal(body, move)
if err != nil {
return err
}
err = move.Valid()
if err != nil {
return err
}
select {
case ap.respChan <- move:
return nil
default:
}

return errors.New("too late to play!")
}

func (ap *AsyncPlayer) String() string {
return "human"
}