blob: 2b3ece5a8e397f43116e66ec0a13a9749b78bc1e [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help
package main
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/vtrace"
"v.io/x/lib/cmdline"
"v.io/x/ref/examples/rps"
"v.io/x/ref/examples/rps/internal"
"v.io/x/ref/internal/logger"
"v.io/x/ref/lib/v23cmd"
_ "v.io/x/ref/runtime/factories/roaming"
)
var name, aclFile, mountPrefix string
func main() {
cmdRoot.Flags.StringVar(&name, "name", "", "Identifier to publish as (defaults to principal's blessing names).")
cmdRoot.Flags.StringVar(&aclFile, "acl-file", "", "File containing JSON-encoded Permissions.")
cmdRoot.Flags.StringVar(&mountPrefix, "mount-prefix", "vlab", "The mount prefix to use. The published name will be <mount-prefix>/rps/player/<name>.")
cmdline.HideGlobalFlagsExcept()
cmdline.Main(cmdRoot)
}
var cmdRoot = &cmdline.Command{
Runner: v23cmd.RunnerFunc(runPlayer),
Name: "rpsplayer",
Short: "Implements the Player interface",
Long: `
Command rpsplayer implements the Player interface, which enables a human to play
the game.
`,
}
func runPlayer(rootctx *context.T, env *cmdline.Env, args []string) error {
for {
ctx, _ := vtrace.WithNewTrace(rootctx)
if selectOne([]string{"Initiate Game", "Wait For Challenge"}) == 0 {
initiateGame(ctx)
} else {
fmt.Println("Waiting to receive a challenge...")
game := recvChallenge(ctx)
playGame(ctx, game.address, game.id)
}
if selectOne([]string{"Play Again", "Quit"}) == 1 {
break
}
}
return nil
}
type gameChallenge struct {
address string
id rps.GameId
opts rps.GameOptions
}
// impl is a PlayerServerMethods implementation that prompts the user to accept
// or decline challenges. While waiting for a reply from the user, any incoming
// challenges are auto-declined.
type impl struct {
ch chan gameChallenge
decline bool
lock sync.Mutex
}
func (i *impl) setDecline(v bool) bool {
i.lock.Lock()
defer i.lock.Unlock()
prev := i.decline
i.decline = v
return prev
}
func (i *impl) Challenge(ctx *context.T, call rpc.ServerCall, address string, id rps.GameId, opts rps.GameOptions) error {
remote, _ := security.RemoteBlessingNames(ctx, call.Security())
ctx.VI(1).Infof("Challenge (%q, %+v) from %v", address, id, remote)
// When setDecline(true) returns, future challenges will be declined.
// Whether the current challenge should be considered depends on the
// previous state. If 'decline' was already true, we need to decline
// this challenge. It 'decline' was false, this is the first challenge
// that we should process.
if i.setDecline(true) {
return errors.New("player is busy")
}
fmt.Println()
fmt.Printf("Challenge received from %v for a %d-round ", remote, opts.NumRounds)
switch opts.GameType {
case rps.Classic:
fmt.Print("Classic ")
case rps.LizardSpock:
fmt.Print("Lizard-Spock ")
default:
}
fmt.Println("Game.")
if selectOne([]string{"Accept", "Decline"}) == 0 {
i.ch <- gameChallenge{address, id, opts}
return nil
}
// Start considering challenges again.
i.setDecline(false)
return errors.New("player declined challenge")
}
// recvChallenge runs a server until a game challenge is accepted by the user.
// The server is stopped afterwards.
func recvChallenge(ctx *context.T) gameChallenge {
ch := make(chan gameChallenge)
if name == "" {
name = internal.CreateName(ctx)
}
fullname := naming.Join(mountPrefix, "rps", "player", name)
service := rps.PlayerServer(&impl{ch: ch})
auth := internal.NewAuthorizer(aclFile)
ctx, cancel := context.WithCancel(ctx)
ctx, server, err := v23.WithNewServer(ctx, fullname, service, auth)
if err != nil {
ctx.Fatalf("NewServer failed: %v", err)
}
ctx.Infof("Listening on endpoint /%s", server.Status().Endpoints[0])
result := <-ch
cancel()
<-server.Closed()
return result
}
// initiateGame initiates a new game by getting a list of judges and players,
// and asking the user to select one of each, to select the game options, what
// to play, etc.
func initiateGame(ctx *context.T) error {
jChan := make(chan []string)
oChan := make(chan []string)
go findAll(ctx, "judge", jChan)
go findAll(ctx, "player", oChan)
fmt.Println("Looking for available participants...")
judges := <-jChan
opponents := <-oChan
fmt.Println()
if len(judges) == 0 || len(opponents) == 0 {
return errors.New("no one to play with")
}
fmt.Println("Choose a judge:")
j := selectOne(judges)
fmt.Println("Choose an opponent:")
o := selectOne(opponents)
fmt.Println("Choose the type of rock-paper-scissors game would you like to play:")
gameType := selectOne([]string{"Classic", "LizardSpock"})
fmt.Println("Choose the number of rounds required to win:")
numRounds := selectOne([]string{"1", "2", "3", "4", "5", "6"}) + 1
gameOpts := rps.GameOptions{NumRounds: int32(numRounds), GameType: rps.GameTypeTag(gameType)}
gameID, err := createGame(ctx, judges[j], gameOpts)
if err != nil {
ctx.Infof("createGame: %v", err)
return err
}
for {
err := sendChallenge(ctx, opponents[o], judges[j], gameID, gameOpts)
if err == nil {
break
}
fmt.Printf("Challenge was declined by %s (%v)\n", opponents[o], err)
fmt.Println("Choose another opponent:")
o = selectOne(opponents)
}
fmt.Println("Joining the game...")
if _, err = playGame(ctx, judges[j], gameID); err != nil {
ctx.Infof("playGame: %v", err)
return err
}
return nil
}
func createGame(ctx *context.T, judge string, opts rps.GameOptions) (rps.GameId, error) {
return rps.JudgeClient(naming.Join(mountPrefix, judge)).CreateGame(ctx, opts)
}
func sendChallenge(ctx *context.T, opponent, judge string, gameID rps.GameId, gameOpts rps.GameOptions) error {
return rps.PlayerClient(naming.Join(mountPrefix, opponent)).Challenge(ctx, judge, gameID, gameOpts)
}
func playGame(outer *context.T, judge string, gameID rps.GameId) (rps.PlayResult, error) {
ctx, cancel := context.WithTimeout(outer, 10*time.Minute)
defer cancel()
fmt.Println()
game, err := rps.JudgeClient(naming.Join(mountPrefix, judge)).Play(ctx, gameID)
if err != nil {
return rps.PlayResult{}, err
}
var playerNum int32
rStream := game.RecvStream()
for rStream.Advance() {
in := rStream.Value()
switch v := in.(type) {
case rps.JudgeActionPlayerNum:
playerNum = v.Value
fmt.Printf("You are player %d\n", playerNum)
case rps.JudgeActionOpponentName:
fmt.Printf("Your opponent is %q\n", v.Value)
case rps.JudgeActionRoundResult:
rr := v.Value
if playerNum != 1 && playerNum != 2 {
ctx.Fatalf("invalid playerNum: %d", playerNum)
}
fmt.Printf("You played %q\n", rr.Moves[playerNum-1])
fmt.Printf("Your opponent played %q\n", rr.Moves[2-playerNum])
if len(rr.Comment) > 0 {
fmt.Printf(">>> %s <<<\n", strings.ToUpper(rr.Comment))
}
if rr.Winner == 0 {
fmt.Println("----- It's a draw -----")
} else if rps.WinnerTag(playerNum) == rr.Winner {
fmt.Println("***** You WIN *****")
} else {
fmt.Println("##### You LOSE #####")
}
case rps.JudgeActionMoveOptions:
opts := v.Value
fmt.Println()
fmt.Println("Choose your weapon:")
m := selectOne(opts)
if err := game.SendStream().Send(rps.PlayerActionMove{Value: opts[m]}); err != nil {
return rps.PlayResult{}, err
}
case rps.JudgeActionScore:
score := v.Value
fmt.Println()
fmt.Println("==== GAME SUMMARY ====")
fmt.Print(internal.FormatScoreCard(score))
fmt.Println("======================")
if rps.WinnerTag(playerNum) == score.Winner {
fmt.Println("You won! :)")
} else {
fmt.Println("You lost! :(")
}
default:
outer.Infof("unexpected message type: %T", in)
}
}
if err := rStream.Err(); err == nil {
fmt.Println("Game Ended")
} else {
outer.Infof("stream error: %v", err)
}
return game.Finish()
}
func selectOne(choices []string) (choice int) {
if len(choices) == 0 {
logger.Global().Fatal("No options to choose from!")
}
fmt.Println()
for i, x := range choices {
fmt.Printf(" %d. %s\n", i+1, x)
}
fmt.Println()
for {
if len(choices) == 1 {
fmt.Print("Select one [1] --> ")
} else {
fmt.Printf("Select one [1-%d] --> ", len(choices))
}
fmt.Scanf("%d", &choice)
if choice >= 1 && choice <= len(choices) {
choice -= 1
break
}
}
fmt.Println()
return
}
func findAll(ctx *context.T, t string, out chan []string) {
ns := v23.GetNamespace(ctx)
var result []string
c, err := ns.Glob(ctx, naming.Join(mountPrefix, "rps", t, "*"))
if err != nil {
ctx.Infof("ns.Glob failed: %v", err)
out <- result
return
}
for e := range c {
switch v := e.(type) {
case *naming.GlobReplyError:
fmt.Print("E")
case *naming.GlobReplyEntry:
fmt.Print(".")
result = append(result, strings.TrimPrefix(v.Value.Name, naming.Clean(mountPrefix)+"/"))
}
}
if len(result) == 0 {
ctx.Infof("found no %ss", t)
out <- result
return
}
sort.Strings(result)
out <- result
}