Adding basic arrange players.
Discovery changes to align with Croupier Flutter.
Change-Id: Ia744cd9174a740532fcd6a0896aaf21423d9c13c
diff --git a/go/src/hearts/assets/SitSpot.png b/go/src/hearts/assets/SitSpot.png
new file mode 100644
index 0000000..3d66766
--- /dev/null
+++ b/go/src/hearts/assets/SitSpot.png
Binary files differ
diff --git a/go/src/hearts/assets/WatchSpot.png b/go/src/hearts/assets/WatchSpot.png
new file mode 100644
index 0000000..440011f
--- /dev/null
+++ b/go/src/hearts/assets/WatchSpot.png
Binary files differ
diff --git a/go/src/hearts/img/texture/texture.go b/go/src/hearts/img/texture/texture.go
index 3179eca..08c3fff 100644
--- a/go/src/hearts/img/texture/texture.go
+++ b/go/src/hearts/img/texture/texture.go
@@ -275,7 +275,7 @@
"R-Lower-Gray.png", "S-Lower-Gray.png", "T-Lower-Gray.png", "U-Lower-Gray.png", "V-Lower-Gray.png", "W-Lower-Gray.png",
"X-Lower-Gray.png", "Y-Lower-Gray.png", "Z-Lower-Gray.png", "Space-Gray.png", "RoundedRectangle-DBlue.png",
"RoundedRectangle-LBlue.png", "RoundedRectangle-Gray.png", "Rectangle-LBlue.png", "Rectangle-DBlue.png", "HorizontalPullTab.png",
- "VerticalPullTab.png", "NewGame.png", "NewRound.png", "JoinGame.png", "Deal.png", "Period.png",
+ "VerticalPullTab.png", "NewGame.png", "NewRound.png", "JoinGame.png", "Deal.png", "Period.png", "SitSpot.png", "WatchSpot.png",
}
for _, f := range boundedImgs {
a, err := asset.Open(f)
diff --git a/go/src/hearts/img/uistate/uistate.go b/go/src/hearts/img/uistate/uistate.go
index d3c8aec..441b7b8 100644
--- a/go/src/hearts/img/uistate/uistate.go
+++ b/go/src/hearts/img/uistate/uistate.go
@@ -83,13 +83,15 @@
CurPlayerIndex int // the player number of this player
Ctx *context.T
Service syncbase.Service
- Debug bool // true if debugging, adds extra functionality to switch between players
- Shutdown func() // used to shut down a v23.Init()
- GameID int // used to differentiate between concurrent games
- IsOwner bool // true if this player is the game creator
- AnimChans []chan bool // keeps track of all 'quit' channels in animations so their goroutines can be stopped
- SGChan chan bool // pass in a bool to stop advertising the syncgroup
- ScanChan chan bool // pass in a bool to stop scanning for syncgroups
+ Debug bool // true if debugging, adds extra functionality to switch between players
+ Shutdown func() // used to shut down a v23.Init()
+ GameID int // used to differentiate between concurrent games
+ IsOwner bool // true if this player is the game creator
+ UserData map[int]map[string]interface{} // user data indexed by user ID
+ PlayerData map[int]map[string]interface{} // user data indexed by player number
+ AnimChans []chan bool // keeps track of all 'quit' channels in animations so their goroutines can be stopped
+ SGChan chan bool // pass in a bool to stop advertising the syncgroup
+ ScanChan chan bool // pass in a bool to stop scanning for syncgroups
}
func MakeUIState() *UIState {
@@ -118,6 +120,8 @@
CurView: None,
Done: false,
Debug: false,
+ UserData: make(map[int]map[string]interface{}, 0),
+ PlayerData: make(map[int]map[string]interface{}, 0),
AnimChans: make([]chan bool, 0),
}
}
diff --git a/go/src/hearts/img/view/view.go b/go/src/hearts/img/view/view.go
index b2cdf11..9718992 100644
--- a/go/src/hearts/img/view/view.go
+++ b/go/src/hearts/img/view/view.go
@@ -25,17 +25,64 @@
"golang.org/x/mobile/exp/sprite"
)
-// TODO(emshack): Flesh out Arrange view to actually arrange players
func LoadArrangeView(u *uistate.UIState) {
u.CurView = uistate.Arrange
<-time.After(1 * time.Second)
resetAnims(u)
resetImgs(u)
resetScene(u)
- buttonPos := coords.MakeVec((u.WindowSize.X-2*u.CardDim.X)/2, (u.WindowSize.Y-u.CardDim.Y)/2)
- buttonDim := coords.MakeVec(2*u.CardDim.X, u.CardDim.Y)
- buttonImage := u.Texs["Deal.png"]
- u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(buttonImage, buttonPos, buttonDim, u.Eng, u.Scene))
+ addHeader(u)
+ sitImg := u.Texs["SitSpot.png"]
+ watchImg := u.Texs["WatchSpot.png"]
+ arrangeBlockLength := u.WindowSize.X - 4*u.Padding
+ if u.WindowSize.Y < u.WindowSize.X {
+ arrangeBlockLength = u.WindowSize.Y - 4*u.Padding
+ }
+ arrangeDim := coords.MakeVec(arrangeBlockLength/3-4*u.Padding, arrangeBlockLength/3-4*u.Padding)
+ nilDim := coords.MakeVec(0, 0)
+ // player 0 seat
+ sitPos := coords.MakeVec((u.WindowSize.X-arrangeDim.X)/2, u.WindowSize.Y-arrangeDim.Y-2*u.Padding)
+ if u.PlayerData[0] == nil {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, arrangeDim, u.Eng, u.Scene))
+ } else {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, nilDim, u.Eng, u.Scene))
+ avatarKey := u.PlayerData[0]["avatar"].(string)
+ avatar := u.Texs[avatarKey]
+ u.BackgroundImgs = append(u.BackgroundImgs, texture.MakeImgWithoutAlt(avatar, sitPos, arrangeDim, u.Eng, u.Scene))
+ }
+ // player 1 seat
+ sitPos = coords.MakeVec((u.WindowSize.X-arrangeDim.X)/2-arrangeDim.X-2*u.Padding, u.WindowSize.Y-2*arrangeDim.Y-4*u.Padding)
+ if u.PlayerData[1] == nil {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, arrangeDim, u.Eng, u.Scene))
+ } else {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, nilDim, u.Eng, u.Scene))
+ avatarKey := u.PlayerData[1]["avatar"].(string)
+ avatar := u.Texs[avatarKey]
+ u.BackgroundImgs = append(u.BackgroundImgs, texture.MakeImgWithoutAlt(avatar, sitPos, arrangeDim, u.Eng, u.Scene))
+ }
+ // player 2 seat
+ sitPos = coords.MakeVec((u.WindowSize.X-arrangeDim.X-2*u.Padding)/2, u.WindowSize.Y-3*arrangeDim.Y-6*u.Padding)
+ if u.PlayerData[2] == nil {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, arrangeDim, u.Eng, u.Scene))
+ } else {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, nilDim, u.Eng, u.Scene))
+ avatarKey := u.PlayerData[2]["avatar"].(string)
+ avatar := u.Texs[avatarKey]
+ u.BackgroundImgs = append(u.BackgroundImgs, texture.MakeImgWithoutAlt(avatar, sitPos, arrangeDim, u.Eng, u.Scene))
+ }
+ // player 3 seat
+ sitPos = coords.MakeVec((u.WindowSize.X-arrangeDim.X)/2+arrangeDim.X+2*u.Padding, u.WindowSize.Y-2*arrangeDim.Y-4*u.Padding)
+ if u.PlayerData[3] == nil {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, arrangeDim, u.Eng, u.Scene))
+ } else {
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(sitImg, sitPos, nilDim, u.Eng, u.Scene))
+ avatarKey := u.PlayerData[3]["avatar"].(string)
+ avatar := u.Texs[avatarKey]
+ u.BackgroundImgs = append(u.BackgroundImgs, texture.MakeImgWithoutAlt(avatar, sitPos, arrangeDim, u.Eng, u.Scene))
+ }
+ // table
+ watchPos := coords.MakeVec((u.WindowSize.X-arrangeDim.X)/2, u.WindowSize.Y-2*arrangeDim.Y-4*u.Padding)
+ u.Buttons = append(u.Buttons, texture.MakeImgWithoutAlt(watchImg, watchPos, arrangeDim, u.Eng, u.Scene))
}
// Waiting view: Displays the word "Waiting". To be displayed when players are waiting for a new round to be dealt
@@ -388,6 +435,7 @@
}
}
+// TODO(emshack): When go mobile implements sprite.engine.Unregister, use this instead
func ChangePlayMessage(message string, u *uistate.UIState) {
// remove text and replace with message
var emptyTex sprite.SubTex
diff --git a/go/src/hearts/logic/player/player.go b/go/src/hearts/logic/player/player.go
index 4f61379..ce96d41 100644
--- a/go/src/hearts/logic/player/player.go
+++ b/go/src/hearts/logic/player/player.go
@@ -119,6 +119,18 @@
p.hand = cards
}
+func (p *Player) SetName(name string) {
+ p.playerName = name
+}
+
+func (p *Player) SetIconImage(image sprite.SubTex) {
+ p.playerIconImage = image
+}
+
+func (p *Player) SetDeviceImage(image sprite.SubTex) {
+ p.playerDeviceImage = image
+}
+
// Sets passedTo of p to cards
func (p *Player) SetPassedTo(cards []*card.Card) {
p.passedTo = cards
diff --git a/go/src/hearts/main.go b/go/src/hearts/main.go
index e51485c..b82a34e 100644
--- a/go/src/hearts/main.go
+++ b/go/src/hearts/main.go
@@ -109,7 +109,8 @@
u.CurTable = table.InitializeGame(u.NumPlayers, u.Texs)
server.CreateTables(u)
// Create watch stream to update game state based on Syncbase updates
- go watch.Update(u)
+ go watch.UpdateGame(u)
+ go watch.UpdateSettings(u)
}
func onStop(u *uistate.UIState) {
diff --git a/go/src/hearts/syncbase/client/main.go b/go/src/hearts/syncbase/client/main.go
index 8fccb35..46ddf10 100644
--- a/go/src/hearts/syncbase/client/main.go
+++ b/go/src/hearts/syncbase/client/main.go
@@ -7,11 +7,13 @@
package client
import (
- "encoding/json"
"fmt"
+ "strconv"
+ "strings"
+
"hearts/img/uistate"
"hearts/syncbase/util"
- "strings"
+
"v.io/v23/context"
"v.io/v23/discovery"
wire "v.io/v23/services/syncbase/nosql"
@@ -59,7 +61,7 @@
instances[string(found.Service.InstanceId)] = found.Service.InstanceName
fmt.Printf("Discovered %q: Instance=%x, Interface=%q, Addrs=%v\n", found.Service.InstanceName, found.Service.InstanceId, found.Service.InterfaceName, found.Service.Addrs)
if found.Service.InterfaceName == util.CroupierInterface {
- return found.Service.Addrs
+ return []string{found.Service.Attrs["settings_sgname"], found.Service.Addrs[0]}
}
case discovery.UpdateLost:
lost := u.Value
@@ -73,15 +75,14 @@
return nil
}
-// Returns a watchstream of the gamelog data
-func WatchData(u *uistate.UIState) (nosql.WatchStream, error) {
+// Returns a watchstream of the data in the table
+func WatchData(tableName, prefix string, u *uistate.UIState) (nosql.WatchStream, error) {
db := u.Service.App(util.AppName).NoSQLDatabase(util.DbName, nil)
- prefix := ""
resumeMarker, err := db.GetResumeMarker(u.Ctx)
if err != nil {
fmt.Println("RESUMEMARKER ERR: ", err)
}
- return db.Watch(u.Ctx, util.LogName, prefix, resumeMarker)
+ return db.Watch(u.Ctx, tableName, prefix, resumeMarker)
}
// Joins a set of gamelog and game settings syncgroups
@@ -103,15 +104,8 @@
fmt.Println("Syncgroup joined")
// Set UIState GameID
tmp := strings.Split(logName, "-")
- lasttmp := tmp[len(tmp)-1]
- tmpMap := make(map[string]interface{})
- err = json.Unmarshal([]byte(lasttmp), &tmpMap)
- if err != nil {
- fmt.Println("ERROR UNMARSHALLING")
- }
- u.GameID = int(tmpMap["gameID"].(float64))
- u.CurPlayerIndex = NumInSG(logName, u) - 1
- fmt.Println(u.CurPlayerIndex)
+ gameID, _ := strconv.Atoi(tmp[len(tmp)-1])
+ u.GameID = gameID
ch <- true
}
}
diff --git a/go/src/hearts/syncbase/gamelog/logWriter.go b/go/src/hearts/syncbase/gamelog/logWriter.go
index d17d163..2cbeeb7 100644
--- a/go/src/hearts/syncbase/gamelog/logWriter.go
+++ b/go/src/hearts/syncbase/gamelog/logWriter.go
@@ -14,6 +14,7 @@
"hearts/img/uistate"
"hearts/logic/card"
"hearts/syncbase/server"
+ "hearts/syncbase/util"
"v.io/v23/context"
"v.io/v23/syncbase"
@@ -87,7 +88,13 @@
return logKeyValue(u.Service, u.Ctx, key, value)
}
-// Note: The + is syntax used to replicate the way Croupier in Dart/Flutter writes keys.
+func LogPlayerNum(u *uistate.UIState) bool {
+ key := strconv.Itoa(u.GameID) + "/players/" + strconv.Itoa(util.UserID) + "/player_number"
+ value := strconv.Itoa(u.CurPlayerIndex)
+ return logKeyValue(u.Service, u.Ctx, key, value)
+}
+
+// Note: The syntax replicates the way Croupier in Dart/Flutter writes keys.
func getKey(playerId int, u *uistate.UIState) string {
t := int(time.Now().UnixNano() / 1000000)
key := strconv.Itoa(u.GameID) + "/log/" + strconv.Itoa(t) + Dash + strconv.Itoa(playerId)
diff --git a/go/src/hearts/syncbase/server/main.go b/go/src/hearts/syncbase/server/main.go
index 1a711c2..4c3a0f0 100644
--- a/go/src/hearts/syncbase/server/main.go
+++ b/go/src/hearts/syncbase/server/main.go
@@ -9,9 +9,11 @@
import (
"encoding/json"
"fmt"
+ "math/rand"
+
"hearts/img/uistate"
"hearts/syncbase/util"
- "math/rand"
+
"v.io/v23/context"
"v.io/v23/discovery"
"v.io/v23/security"
@@ -25,7 +27,7 @@
)
// Advertises a set of game log and game settings syncgroups
-func Advertise(logAddress, settingsAddress string, quit chan bool, ctx *context.T) {
+func Advertise(logAddress, settingsAddress, gameStartData string, quit chan bool, ctx *context.T) {
ctx, stop := context.WithCancel(ctx)
mdns, err := mdns.New("")
if err != nil {
@@ -35,9 +37,9 @@
gameService := discovery.Service{
InstanceName: "A sample game service",
InterfaceName: util.CroupierInterface,
- Addrs: []string{settingsAddress, logAddress},
+ Attrs: map[string]string{"settings_sgname": settingsAddress, "game_start_data": gameStartData},
+ Addrs: []string{logAddress},
}
- fmt.Println(gameService)
if _, err := discoveryService.Advertise(ctx, &gameService, nil); err != nil {
ctx.Fatalf("Advertise failed: %v", err)
}
@@ -49,7 +51,7 @@
}
}
-// Puts key and value into the syncbase table
+// Puts key and value into the syncbase gamelog table
func AddKeyValue(service syncbase.Service, ctx *context.T, key, value string) bool {
app := service.App(util.AppName)
db := app.NoSQLDatabase(util.DbName, nil)
@@ -104,6 +106,7 @@
settingsMap["avatar"] = util.UserAvatar
settingsMap["name"] = util.UserName
settingsMap["color"] = util.UserColor
+ u.UserData[util.UserID] = settingsMap
value, err := json.Marshal(settingsMap)
if err != nil {
fmt.Println("WE HAVE A HUGE PROBLEM:", err)
@@ -115,7 +118,6 @@
func CreateSyncgroups(ch chan string, u *uistate.UIState) {
fmt.Println("Creating Syncgroup")
u.IsOwner = true
- u.CurPlayerIndex = 0
// Generate random gameID information to advertise this game
gameID := rand.Intn(1000000)
gameMap := make(map[string]interface{})
@@ -127,8 +129,9 @@
if err != nil {
fmt.Println("WE HAVE A HUGE PROBLEM:", err)
}
+ ch <- string(value)
// Create gamelog syncgroup
- logSGName := util.MountPoint + "/croupier/" + util.SBName + "/%%sync/gaming-" + string(value)
+ logSGName := fmt.Sprintf("%s/croupier/%s/%%%%sync/gaming-%d", util.MountPoint, util.SBName, gameID)
allAccess := access.AccessList{In: []security.BlessingPattern{"..."}}
permissions := access.Permissions{
"Admin": allAccess,
diff --git a/go/src/hearts/syncbase/watch/watch.go b/go/src/hearts/syncbase/watch/watch.go
index 9d48658..f749591 100644
--- a/go/src/hearts/syncbase/watch/watch.go
+++ b/go/src/hearts/syncbase/watch/watch.go
@@ -10,6 +10,7 @@
package watch
import (
+ "encoding/json"
"fmt"
"hearts/img/direction"
"hearts/img/reposition"
@@ -18,14 +19,15 @@
"hearts/logic/card"
"hearts/syncbase/client"
"hearts/syncbase/gamelog"
+ "hearts/syncbase/util"
"strconv"
"strings"
"time"
"v.io/v23/syncbase/nosql"
)
-func Update(u *uistate.UIState) {
- stream, err := client.WatchData(u)
+func UpdateSettings(u *uistate.UIState) {
+ stream, err := client.WatchData(util.SettingsName, "users", u)
if err != nil {
fmt.Println("WatchData error:", err)
}
@@ -37,26 +39,76 @@
if err := c.Value(&value); err != nil {
fmt.Println("Value error:", err)
}
+ var valueMap map[string]interface{}
+ err := json.Unmarshal(value, &valueMap)
+ if err != nil {
+ fmt.Println("Unmarshal error:", err)
+ }
+ key := c.Row
+ fmt.Println(key, string(value))
+ userID, _ := strconv.Atoi(strings.Split(key, "/")[1])
+ u.UserData[userID] = valueMap
+ } else {
+ fmt.Println("Unexpected ChangeType: ", c.ChangeType)
+ }
+ }
+ }
+}
+
+func UpdateGame(u *uistate.UIState) {
+ stream, err := client.WatchData(util.LogName, "", u)
+ if err != nil {
+ fmt.Println("WatchData error:", err)
+ }
+ for {
+ if updateExists := stream.Advance(); updateExists {
+ c := stream.Change()
+ if c.ChangeType == nosql.PutChange {
+ key := c.Row
+ var value []byte
+ if err := c.Value(&value); err != nil {
+ fmt.Println("Value error:", err)
+ }
valueStr := string(value)
- fmt.Println(valueStr)
- updateType := strings.Split(valueStr, "|")[0]
- switch updateType {
- case gamelog.Deal:
- go onDeal(valueStr, u)
- case gamelog.Pass:
- go onPass(valueStr, u)
- case gamelog.Take:
- go onTake(valueStr, u)
- case gamelog.Play:
- go onPlay(valueStr, u)
- case gamelog.Ready:
- go onReady(valueStr, u)
+ keyType := strings.Split(key, "/")[1]
+ switch keyType {
+ case "log":
+ updateType := strings.Split(valueStr, "|")[0]
+ switch updateType {
+ case gamelog.Deal:
+ onDeal(valueStr, u)
+ case gamelog.Pass:
+ onPass(valueStr, u)
+ case gamelog.Take:
+ onTake(valueStr, u)
+ case gamelog.Play:
+ onPlay(valueStr, u)
+ case gamelog.Ready:
+ onReady(valueStr, u)
+ }
+ case "players":
+ onPlayers(key, valueStr, u)
}
} else {
fmt.Println("Unexpected ChangeType: ", c.ChangeType)
}
}
- fmt.Println(stream.Err())
+ }
+}
+
+func onPlayers(key, value string, u *uistate.UIState) {
+ userID, _ := strconv.Atoi(strings.Split(key, "/")[2])
+ playerNum, _ := strconv.Atoi(value)
+ u.PlayerData[playerNum] = u.UserData[userID]
+ user := u.UserData[userID]
+ if user != nil {
+ img := u.Texs[user["avatar"].(string)]
+ name := user["name"].(string)
+ u.CurTable.GetPlayers()[playerNum].SetIconImage(img)
+ u.CurTable.GetPlayers()[playerNum].SetName(name)
+ }
+ if u.CurView == uistate.Arrange {
+ view.LoadArrangeView(u)
}
}
diff --git a/go/src/hearts/touchhandler/touchhandler.go b/go/src/hearts/touchhandler/touchhandler.go
index 78b389b..69c104a 100644
--- a/go/src/hearts/touchhandler/touchhandler.go
+++ b/go/src/hearts/touchhandler/touchhandler.go
@@ -8,8 +8,10 @@
import (
"fmt"
+
"golang.org/x/mobile/event/touch"
"golang.org/x/mobile/exp/sprite"
+
"hearts/img/coords"
"hearts/img/reposition"
"hearts/img/staticimg"
@@ -106,13 +108,14 @@
if buttonList[0] == u.Buttons[0] {
ch := make(chan string)
go server.CreateSyncgroups(ch, u)
+ gameStartData := <-ch
logName := <-ch
settingsName := <-ch
if logName != "" && settingsName != "" {
u.ScanChan <- true
u.ScanChan = nil
u.SGChan = make(chan bool)
- go server.Advertise(logName, settingsName, u.SGChan, u.Ctx)
+ go server.Advertise(logName, settingsName, gameStartData, u.SGChan, u.Ctx)
view.LoadArrangeView(u)
}
} else {
@@ -137,8 +140,16 @@
func beginClickArrange(t touch.Event, u *uistate.UIState) {
buttonList := findClickedButton(t, u)
- if len(buttonList) > 0 {
- gamelog.LogReady(u)
+ if len(buttonList) > 0 && u.PlayerData[u.CurPlayerIndex] == nil {
+ for i, b := range u.Buttons {
+ if buttonList[0] == b {
+ u.CurPlayerIndex = i
+ }
+ }
+ if u.CurPlayerIndex < 4 {
+ gamelog.LogReady(u)
+ gamelog.LogPlayerNum(u)
+ }
view.LoadWaitingView(u)
}
}