diff --git a/pazaakcli/cmd/humancli/main.go b/pazaakcli/cmd/humancli/main.go new file mode 100644 index 0000000..d5b6eea --- /dev/null +++ b/pazaakcli/cmd/humancli/main.go @@ -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) + } + +} diff --git a/pazaakcli/cmd/multipaz/main.go b/pazaakcli/cmd/multipaz/main.go new file mode 100644 index 0000000..4985f34 --- /dev/null +++ b/pazaakcli/cmd/multipaz/main.go @@ -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()}) + } +} diff --git a/pazaakcli/main.go b/pazaakcli/main.go index e598f6e..031fcfa 100644 --- a/pazaakcli/main.go +++ b/pazaakcli/main.go @@ -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) diff --git a/pazaakcli/pazaak/pazaak.go b/pazaakcli/pazaak/pazaak.go index 5c12ae6..560d69e 100644 --- a/pazaakcli/pazaak/pazaak.go +++ b/pazaakcli/pazaak/pazaak.go @@ -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:"-"` @@ -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()) } @@ -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 } @@ -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: @@ -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) } diff --git a/pazaakcli/player/player.go b/pazaakcli/player/player.go index 45715a0..54a5123 100644 --- a/pazaakcli/player/player.go +++ b/pazaakcli/player/player.go @@ -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 { @@ -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) @@ -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") @@ -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" +}