Put switch chan case in goroutine
Change-Id: I8999f36d237600ffd694a0bb81bf9d87d35d1894
diff --git a/go/Makefile b/go/Makefile
index f49facf..2144da3 100644
--- a/go/Makefile
+++ b/go/Makefile
@@ -26,12 +26,10 @@
delete:
rm -rf tmp/$(name)
-# Naming collisions for different instances of syncbase for the same user?
-# Easy way to make --v23.permissions.literal?
.PHONY:
syncbase: delete bin/syncbased credentials tmp
$(eval blessing := $(shell bin/principal dump --v23.credentials=./credentials -s=true))
- $(eval email := $(subst dev.v.io/u/,,$(blessing)))
+ $(eval email := $(subst dev.v.io:u:,,$(blessing)))
bin/syncbased \
--v=5 \
--alsologtostderr=false \
@@ -52,4 +50,5 @@
all: fmt
- #--name=/192.168.86.254:8101/croupier/$(name) \
\ No newline at end of file
+# Switch mountpoints by replacing the --name flag in syncbase with the following line:
+# --name=/192.168.86.254:8101/croupier/$(name) \
\ No newline at end of file
diff --git a/go/src/hearts/assets/Period.png b/go/src/hearts/assets/Period.png
new file mode 100644
index 0000000..10e1329
--- /dev/null
+++ b/go/src/hearts/assets/Period.png
Binary files differ
diff --git a/go/src/hearts/img/reposition/reposition.go b/go/src/hearts/img/reposition/reposition.go
index 449eae8..9d894b1 100644
--- a/go/src/hearts/img/reposition/reposition.go
+++ b/go/src/hearts/img/reposition/reposition.go
@@ -62,7 +62,7 @@
func SetTableDropColors(u *uistate.UIState) {
blueTargetIndex := u.CurTable.WhoseTurn()
for i, d := range u.DropTargets {
- if i == blueTargetIndex {
+ if i == blueTargetIndex && u.CurTable.AllDonePassing() {
u.Eng.SetSubTex(d.GetNode(), d.GetAlt())
d.SetDisplayingImage(false)
} else {
@@ -106,46 +106,52 @@
}
// Animation for the 'pass' action, when app is in the table view
-func AnimateTableCardPass(cards []*card.Card, toPlayer int, u *uistate.UIState) {
+func AnimateTableCardPass(cards []*card.Card, toPlayer int, quit chan bool, u *uistate.UIState) {
for cardNum, animCard := range cards {
- cardDim := animCard.GetDimensions()
- dropTargetXY := u.DropTargets[toPlayer].GetCurrent()
- dropTargetDim := u.DropTargets[toPlayer].GetDimensions()
- targetCenter := dropTargetXY.PlusVec(dropTargetDim.DividedBy(2))
- xPlayerBlockSize := 2*u.PlayerIconDim.X + u.Padding
- yPlayerBlockSize := u.TopPadding + 2*u.TableCardDim.Y + 3*u.Padding + u.PlayerIconDim.Y
- blockEdge := targetCenter.MinusVec(cardDim.Times(1.5).Plus(u.Padding))
- var destination *coords.Vec
- switch toPlayer {
- case 0:
- destination = coords.MakeVec(
- blockEdge.X+float32(cardNum)*(u.Padding+cardDim.X),
- u.WindowSize.Y-yPlayerBlockSize-u.TableCardDim.Y)
- case 1:
- destination = coords.MakeVec(
- xPlayerBlockSize,
- blockEdge.Y+float32(cardNum)*(u.Padding+cardDim.Y))
- case 2:
- destination = coords.MakeVec(
- blockEdge.X+float32(cardNum)*(u.Padding+cardDim.X),
- yPlayerBlockSize)
- case 3:
- destination = coords.MakeVec(
- u.WindowSize.X-xPlayerBlockSize-u.TableCardDim.X,
- blockEdge.Y+float32(cardNum)*(u.Padding+cardDim.Y))
- }
+ destination := DetermineTablePassPosition(animCard, cardNum, toPlayer, u)
if cardNum < len(cards)-1 {
- animateCardNoChannel(animCard, destination, cardDim, u)
+ animateCardNoChannel(animCard, destination, animCard.GetDimensions(), u)
} else {
c := make(chan bool)
- animateCardMovement(c, animCard, destination, cardDim, u)
- <-c
+ animateCardMovement(c, animCard, destination, animCard.GetDimensions(), u)
+ SwitchOnChan(c, quit, func() {}, u)
}
}
}
+// Returns a vec containing a card's position after being passed to the player with index playerIndex
+func DetermineTablePassPosition(c *card.Card, cardNum, playerIndex int, u *uistate.UIState) *coords.Vec {
+ cardDim := u.TableCardDim
+ dropTargetXY := u.DropTargets[playerIndex].GetCurrent()
+ dropTargetDim := u.DropTargets[playerIndex].GetDimensions()
+ targetCenter := dropTargetXY.PlusVec(dropTargetDim.DividedBy(2))
+ xPlayerBlockSize := 2*u.PlayerIconDim.X + u.Padding
+ yPlayerBlockSize := u.TopPadding + 2*u.TableCardDim.Y + 3*u.Padding + u.PlayerIconDim.Y
+ blockEdge := targetCenter.MinusVec(cardDim.Times(1.5).Plus(u.Padding))
+ var destination *coords.Vec
+ switch playerIndex {
+ case 0:
+ destination = coords.MakeVec(
+ blockEdge.X+float32(cardNum)*(u.Padding+cardDim.X),
+ u.WindowSize.Y-yPlayerBlockSize-u.TableCardDim.Y)
+ case 1:
+ destination = coords.MakeVec(
+ xPlayerBlockSize,
+ blockEdge.Y+float32(cardNum)*(u.Padding+cardDim.Y))
+ case 2:
+ destination = coords.MakeVec(
+ blockEdge.X+float32(cardNum)*(u.Padding+cardDim.X),
+ yPlayerBlockSize)
+ case 3:
+ destination = coords.MakeVec(
+ u.WindowSize.X-xPlayerBlockSize-u.TableCardDim.X,
+ blockEdge.Y+float32(cardNum)*(u.Padding+cardDim.Y))
+ }
+ return destination
+}
+
// Animation for the 'take' action, when app is in the table view
-func AnimateTableCardTake(cards []*card.Card, p *player.Player, u *uistate.UIState) {
+func AnimateTableCardTake(cards []*card.Card, p *player.Player, quit chan bool, u *uistate.UIState) {
for cardNum, animCard := range cards {
destinationPos := p.GetPassedFrom()[cardNum].GetInitial()
if cardNum < len(cards)-1 {
@@ -153,20 +159,20 @@
} else {
c := make(chan bool)
animateCardMovement(c, animCard, destinationPos, animCard.GetDimensions(), u)
- <-c
+ SwitchOnChan(c, quit, func() {}, u)
}
}
}
// Animation for the 'play' action, when app is in the table view
-func AnimateTableCardPlay(animCard *card.Card, playerInt int, u *uistate.UIState) {
+func AnimateTableCardPlay(animCard *card.Card, playerInt int, quit chan bool, u *uistate.UIState) {
destination := u.DropTargets[playerInt]
destinationPos := destination.GetCurrent()
destinationDim := destination.GetDimensions()
ch := make(chan bool)
animateCardMovement(ch, animCard, destinationPos, destinationDim, u)
- <-ch
- animCard.SetFrontDisplay(u.Eng)
+ onDone := func() { animCard.SetFrontDisplay(u.Eng) }
+ SwitchOnChan(ch, quit, onDone, u)
}
// Animation for the 'pass' action, when app is in the hand view
@@ -244,8 +250,8 @@
// Animate playing of a card in the split view
// Should not be called when the player whose hand is being displayed is the player of the card
-func AnimateSplitCardPlay(c *card.Card, player int, u *uistate.UIState) {
- dropTarget := u.DropTargets[(u.CurPlayerIndex+player)%u.NumPlayers]
+func AnimateSplitCardPlay(c *card.Card, player int, quit chan bool, u *uistate.UIState) {
+ dropTarget := u.DropTargets[(player-u.CurPlayerIndex+u.NumPlayers)%u.NumPlayers]
toPos := dropTarget.GetCurrent()
toDim := dropTarget.GetDimensions()
texture.PopulateCardImage(c, u.Texs, u.Eng, u.Scene)
@@ -259,7 +265,7 @@
}
ch := make(chan bool)
animateCardMovement(ch, c, toPos, toDim, u)
- <-ch
+ SwitchOnChan(ch, quit, func() {}, u)
}
func AnimateInSplit(u *uistate.UIState) {
@@ -349,7 +355,7 @@
}
// Animation for when a trick is taken, when app is in the table view
-func AnimateTableCardTakeTrick(cards []*card.Card, dir direction.Direction, u *uistate.UIState) {
+func AnimateTableCardTakeTrick(cards []*card.Card, dir direction.Direction, quit chan bool, u *uistate.UIState) {
for i, animCard := range cards {
destination := determineDestination(animCard, dir, u.WindowSize)
if i < len(cards)-1 {
@@ -357,7 +363,7 @@
} else {
c := make(chan bool)
animateCardMovement(c, animCard, destination, animCard.GetDimensions(), u)
- <-c
+ SwitchOnChan(c, quit, func() {}, u)
}
}
}
@@ -457,12 +463,12 @@
// cardIndex has an X of the total number of cards in hand, and a Y of the position within the hand of the current card
// padding has an X of the padding along the top edge, and a Y of the padding along each other edge
func SetCardPositionTable(c *card.Card, playerIndex int, cardIndex *coords.Vec, u *uistate.UIState) {
- pos := cardPositionTable(playerIndex, cardIndex, u)
+ pos := CardPositionTable(playerIndex, cardIndex, u)
c.SetInitial(pos)
c.Move(pos, u.TableCardDim, u.Eng)
}
-func cardPositionTable(playerIndex int, cardIndex *coords.Vec, u *uistate.UIState) *coords.Vec {
+func CardPositionTable(playerIndex int, cardIndex *coords.Vec, u *uistate.UIState) *coords.Vec {
var x float32
var y float32
switch playerIndex {
@@ -505,3 +511,24 @@
c.SetInitial(pos)
c.Move(pos, u.CardDim, u.Eng)
}
+
+func RemoveAnimChan(ch chan bool, u *uistate.UIState) {
+ for i, c := range u.AnimChans {
+ if ch == c {
+ u.AnimChans = append(u.AnimChans[:i], u.AnimChans[i+1:]...)
+ return
+ }
+ }
+}
+
+func SwitchOnChan(animChan, quitChan chan bool, f func(), u *uistate.UIState) {
+ select {
+ case <-quitChan:
+ RemoveAnimChan(quitChan, u)
+ return
+ case <-animChan:
+ RemoveAnimChan(quitChan, u)
+ f()
+ return
+ }
+}
diff --git a/go/src/hearts/img/texture/texture.go b/go/src/hearts/img/texture/texture.go
index 74f4d0e..3179eca 100644
--- a/go/src/hearts/img/texture/texture.go
+++ b/go/src/hearts/img/texture/texture.go
@@ -61,22 +61,21 @@
imgs := make([]sprite.SubTex, 0)
for _, char := range input {
key := ""
- // if char is a space
if char == 32 {
key += "Space"
} else if char == 33 {
key += "Bang"
- // if char is an apostrophe
} else if char == 39 {
key += "Apostrophe"
- // if char is a colon
+ } else if char == 46 {
+ key += "Period"
} else if char == 58 {
key += "Colon"
- // if char is a number
} else if char >= 48 && char <= 57 {
+ // if char is a number
key += string(char)
- // if char is a letter
} else {
+ // if char is a letter
key += strings.ToUpper(string(char))
if char > 90 {
key += "-Lower"
@@ -276,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",
+ "VerticalPullTab.png", "NewGame.png", "NewRound.png", "JoinGame.png", "Deal.png", "Period.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 02378bc..d3c8aec 100644
--- a/go/src/hearts/img/uistate/uistate.go
+++ b/go/src/hearts/img/uistate/uistate.go
@@ -26,7 +26,7 @@
const (
None View = "None"
- Opening View = "Opening"
+ Arrange View = "Arrange"
Discovery View = "Discovery"
Pass View = "Pass"
Take View = "Take"
@@ -46,10 +46,11 @@
)
type UIState struct {
- StartTime time.Time
- Images *glutil.Images
- Eng sprite.Engine
- Scene *sprite.Node
+ StartTime time.Time
+ Images *glutil.Images
+ Eng sprite.Engine
+ Scene *sprite.Node
+ // the following arrays keep track of all displayed images
Cards []*card.Card
TableCards []*card.Card
BackgroundImgs []*staticimg.StaticImg
@@ -57,34 +58,38 @@
DropTargets []*staticimg.StaticImg
Buttons []*staticimg.StaticImg
Other []*staticimg.StaticImg
- CurCard *card.Card
- CurImg *staticimg.StaticImg
+ CurCard *card.Card // the card that is currently clicked on
+ CurImg *staticimg.StaticImg // the image that is currently clicked on
// lastMouseXY is in Px: divide by pixelsPerPt to get Pt
- LastMouseXY *coords.Vec
- NumPlayers int
- NumSuits int
- CardSize float32
- CardScaler float32
- TopPadding float32
- BottomPadding float32
- // windowSize is in Pt
- WindowSize *coords.Vec
+ LastMouseXY *coords.Vec // the position of the mouse in the most recent frame
+ NumPlayers int
+ NumSuits int
+ // the following variables are used for sizing and positioning specifications
+ CardSize float32
+ CardScaler float32
+ TopPadding float32
+ BottomPadding float32
+ WindowSize *coords.Vec // windowSize is in Pt
CardDim *coords.Vec
TableCardDim *coords.Vec
PlayerIconDim *coords.Vec
PixelsPerPt float32
Overlap *coords.Vec
Padding float32
- CurView View
- CurTable *table.Table
- Done bool
- Texs map[string]sprite.SubTex
- CurPlayerIndex int
+ CurView View // the screen currently being shown to the user
+ CurTable *table.Table // the table of the current game
+ Done bool // true if the app has been quit
+ Texs map[string]sprite.SubTex // map of all loaded images
+ CurPlayerIndex int // the player number of this player
Ctx *context.T
Service syncbase.Service
- Debug bool
- Shutdown func()
- GameID int
+ 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
}
func MakeUIState() *UIState {
@@ -112,7 +117,7 @@
Padding: float32(5),
CurView: None,
Done: false,
- CurPlayerIndex: 0,
- Debug: true,
+ Debug: false,
+ AnimChans: make([]chan bool, 0),
}
}
diff --git a/go/src/hearts/img/view/view.go b/go/src/hearts/img/view/view.go
index f581e75..b2cdf11 100644
--- a/go/src/hearts/img/view/view.go
+++ b/go/src/hearts/img/view/view.go
@@ -25,22 +25,40 @@
"golang.org/x/mobile/exp/sprite"
)
-// Opening View: Only temporary, for debugging, while discovery is not integrated
-func LoadOpeningView(u *uistate.UIState) {
- u.CurView = uistate.Opening
+// 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)
+ 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))
}
+// Waiting view: Displays the word "Waiting". To be displayed when players are waiting for a new round to be dealt
+// TODO(emshack): Integrate this with Arrange view and Score view so that a separate screen is not necessary
+func LoadWaitingView(u *uistate.UIState) {
+ resetAnims(u)
+ resetImgs(u)
+ resetScene(u)
+ center := u.WindowSize.DividedBy(2)
+ maxWidth := u.WindowSize.X - 2*u.Padding
+ scaler := float32(3)
+ textImgs := texture.MakeStringImgCenterAlign("Waiting...", "", "", true, center, scaler, maxWidth, u)
+ for _, img := range textImgs {
+ u.BackgroundImgs = append(u.BackgroundImgs, img)
+ }
+}
+
+// Discovery view: Displays a menu of possible games to join
func LoadDiscoveryView(discChan chan []string, u *uistate.UIState) {
u.CurView = uistate.Discovery
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
newGameImg := u.Texs["NewGame.png"]
newGameDim := coords.MakeVec(2*u.CardDim.X, u.CardDim.Y)
newGamePos := coords.MakeVec((u.WindowSize.X-newGameDim.X)/2, u.TopPadding)
@@ -61,8 +79,9 @@
// Table View: Displays the table. Intended for public devices
func LoadTableView(u *uistate.UIState) {
u.CurView = uistate.Table
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
scaler := float32(4)
maxWidth := 4 * u.TableCardDim.X
// adding four drop targets for trick
@@ -238,6 +257,7 @@
texture.MakeImgWithoutAlt(deviceIconImage, deviceIconPos, deviceIconDim, u.Eng, u.Scene))
//adding cards
for _, p := range u.CurTable.GetPlayers() {
+ // cards in hand
hand := p.GetHand()
for i, c := range hand {
texture.PopulateCardImage(c, u.Texs, u.Eng, u.Scene)
@@ -246,6 +266,29 @@
u.Eng.SetSubTex(c.GetNode(), c.GetBack())
u.TableCards = append(u.TableCards, c)
}
+ // cards that have been passed
+ passed := p.GetPassedTo()
+ for i, c := range passed {
+ var passer int
+ switch u.CurTable.GetDir() {
+ case direction.Right:
+ passer = (p.GetPlayerIndex() + 1) % u.NumPlayers
+ case direction.Across:
+ passer = (p.GetPlayerIndex() + 2) % u.NumPlayers
+ case direction.Left:
+ passer = (p.GetPlayerIndex() + 3) % u.NumPlayers
+ }
+ cardIndexVec := coords.MakeVec(float32(len(hand)+len(passed)), float32(len(hand)+i))
+ initial := reposition.CardPositionTable(passer, cardIndexVec, u)
+ c.SetInitial(initial)
+ if !p.GetDoneTaking() {
+ texture.PopulateCardImage(c, u.Texs, u.Eng, u.Scene)
+ c.SetBackDisplay(u.Eng)
+ pos := reposition.DetermineTablePassPosition(c, i, p.GetPlayerIndex(), u)
+ c.Move(pos, u.TableCardDim, u.Eng)
+ u.TableCards = append(u.TableCards, c)
+ }
+ }
}
if u.Debug {
addDebugBar(u)
@@ -269,8 +312,9 @@
// Score View: Shows current player standings at the end of every round, including the end of the game
func LoadScoreView(roundScores, winners []int, u *uistate.UIState) {
u.CurView = uistate.Score
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
addHeader(u)
addScoreViewHeaderText(u)
addPlayerScores(roundScores, u)
@@ -280,8 +324,9 @@
// Pass View: Shows player's hand and allows them to pass cards
func LoadPassView(u *uistate.UIState) {
u.CurView = uistate.Pass
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
addHeader(u)
addGrayPassBar(u)
addPassDrops(u)
@@ -294,8 +339,9 @@
// Take View: Shows player's hand and allows them to take the cards that have been passed to them
func LoadTakeView(u *uistate.UIState) {
u.CurView = uistate.Take
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
addHeader(u)
addGrayTakeBar(u)
addHand(u)
@@ -310,8 +356,9 @@
// Play View: Shows player's hand and allows them to play cards
func LoadPlayView(u *uistate.UIState) {
u.CurView = uistate.Play
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
addPlaySlot(u)
addHand(u)
addPlayHeader(getTurnText(u), false, u)
@@ -326,8 +373,9 @@
func LoadSplitView(reloading bool, u *uistate.UIState) {
u.CurView = uistate.Split
+ resetAnims(u)
resetImgs(u)
- ResetScene(u)
+ resetScene(u)
addPlayHeader(getTurnText(u), !reloading, u)
addSplitViewPlayerIcons(!reloading, u)
addHand(u)
@@ -818,9 +866,10 @@
u.DropTargets = make([]*staticimg.StaticImg, 0)
u.Buttons = make([]*staticimg.StaticImg, 0)
u.Other = make([]*staticimg.StaticImg, 0)
+ u.CurCard = nil
}
-func ResetScene(u *uistate.UIState) {
+func resetScene(u *uistate.UIState) {
u.Scene = &sprite.Node{}
u.Eng.Register(u.Scene)
u.Eng.SetTransform(u.Scene, f32.Affine{
@@ -829,6 +878,13 @@
})
}
+func resetAnims(u *uistate.UIState) {
+ for _, ch := range u.AnimChans {
+ ch <- true
+ }
+ u.AnimChans = make([]chan bool, 0)
+}
+
func addDebugBar(u *uistate.UIState) {
buttonDim := u.CardDim
debugTableImage := u.Texs["BakuSquare.png"]
diff --git a/go/src/hearts/main.go b/go/src/hearts/main.go
index 2dc70cc..e51485c 100644
--- a/go/src/hearts/main.go
+++ b/go/src/hearts/main.go
@@ -110,8 +110,6 @@
server.CreateTables(u)
// Create watch stream to update game state based on Syncbase updates
go watch.Update(u)
- go watch.PrintStream("Stream 1", u)
- go watch.PrintStream("Stream 2", u)
}
func onStop(u *uistate.UIState) {
@@ -125,7 +123,8 @@
func onPaint(glctx gl.Context, sz size.Event, u *uistate.UIState) {
if u.CurView == uistate.None {
discChan := make(chan []string)
- go client.ScanForSG(discChan, u.Ctx)
+ u.ScanChan = make(chan bool)
+ go client.ScanForSG(discChan, u.Ctx, u.ScanChan)
view.LoadDiscoveryView(discChan, u)
}
glctx.ClearColor(1, 1, 1, 1)
diff --git a/go/src/hearts/syncbase/client/main.go b/go/src/hearts/syncbase/client/main.go
index 880bf04..8fccb35 100644
--- a/go/src/hearts/syncbase/client/main.go
+++ b/go/src/hearts/syncbase/client/main.go
@@ -7,11 +7,11 @@
package client
import (
- "fmt"
- "strings"
"encoding/json"
+ "fmt"
"hearts/img/uistate"
"hearts/syncbase/util"
+ "strings"
"v.io/v23/context"
"v.io/v23/discovery"
wire "v.io/v23/services/syncbase/nosql"
@@ -23,7 +23,7 @@
)
// Searches for new syncgroups being advertised, sends found syncgroups to sgChan
-func ScanForSG(sgChan chan []string, ctx *context.T) {
+func ScanForSG(sgChan chan []string, ctx *context.T, quit chan bool) {
mdns, err := mdns.New("")
if err != nil {
ctx.Fatalf("Plugin failed: %v", err)
@@ -45,6 +45,8 @@
}
case <-signals.ShutdownOnSignals(ctx):
break loop
+ case <-quit:
+ break loop
}
}
}
@@ -54,19 +56,19 @@
switch u := update.(type) {
case discovery.UpdateFound:
found := u.Value
- instances[string(found.Service.InstanceUuid)] = found.Service.InstanceName
- fmt.Printf("Discovered %q: Instance=%x, Interface=%q, Addrs=%v\n", found.Service.InstanceName, found.Service.InstanceUuid, found.Service.InterfaceName, found.Service.Addrs)
+ 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
}
case discovery.UpdateLost:
lost := u.Value
- name, ok := instances[string(lost.InstanceUuid)]
+ name, ok := instances[string(lost.InstanceId)]
if !ok {
name = "unknown"
}
- delete(instances, string(lost.InstanceUuid))
- fmt.Printf("Lost %q: Instance=%x\n", name, lost.InstanceUuid)
+ delete(instances, string(lost.InstanceId))
+ fmt.Printf("Lost %q: Instance=%x\n", name, lost.InstanceId)
}
return nil
}
@@ -83,8 +85,10 @@
}
// Joins a set of gamelog and game settings syncgroups
+// TODO(emshack): After joining a syncgroup, advertise user settings data
func JoinSyncgroups(ch chan bool, logName, settingsName string, u *uistate.UIState) {
fmt.Println("Joining syncgroup")
+ u.IsOwner = false
app := u.Service.App(util.AppName)
db := app.NoSQLDatabase(util.DbName, nil)
logSg := db.Syncgroup(logName)
@@ -97,6 +101,7 @@
ch <- false
} else {
fmt.Println("Syncgroup joined")
+ // Set UIState GameID
tmp := strings.Split(logName, "-")
lasttmp := tmp[len(tmp)-1]
tmpMap := make(map[string]interface{})
@@ -105,6 +110,19 @@
fmt.Println("ERROR UNMARSHALLING")
}
u.GameID = int(tmpMap["gameID"].(float64))
+ u.CurPlayerIndex = NumInSG(logName, u) - 1
+ fmt.Println(u.CurPlayerIndex)
ch <- true
}
}
+
+func NumInSG(logName string, u *uistate.UIState) int {
+ app := u.Service.App(util.AppName)
+ db := app.NoSQLDatabase(util.DbName, nil)
+ sg := db.Syncgroup(logName)
+ members, err := sg.GetMembers(u.Ctx)
+ if err != nil {
+ fmt.Println(err)
+ }
+ return len(members)
+}
diff --git a/go/src/hearts/syncbase/server/main.go b/go/src/hearts/syncbase/server/main.go
index 0f17dd8..1a711c2 100644
--- a/go/src/hearts/syncbase/server/main.go
+++ b/go/src/hearts/syncbase/server/main.go
@@ -7,11 +7,11 @@
package server
import (
- "math/rand"
"encoding/json"
"fmt"
"hearts/img/uistate"
"hearts/syncbase/util"
+ "math/rand"
"v.io/v23/context"
"v.io/v23/discovery"
"v.io/v23/security"
@@ -25,7 +25,7 @@
)
// Advertises a set of game log and game settings syncgroups
-func Advertise(logAddress, settingsAddress string, ctx *context.T) {
+func Advertise(logAddress, settingsAddress string, quit chan bool, ctx *context.T) {
ctx, stop := context.WithCancel(ctx)
mdns, err := mdns.New("")
if err != nil {
@@ -35,14 +35,18 @@
gameService := discovery.Service{
InstanceName: "A sample game service",
InterfaceName: util.CroupierInterface,
- Addrs: []string{settingsAddress, logAddress},
+ Addrs: []string{settingsAddress, logAddress},
}
fmt.Println(gameService)
- if _, err := discoveryService.Advertise(ctx, gameService, nil); err != nil {
+ if _, err := discoveryService.Advertise(ctx, &gameService, nil); err != nil {
ctx.Fatalf("Advertise failed: %v", err)
}
- <-signals.ShutdownOnSignals(ctx)
- stop()
+ select {
+ case <-signals.ShutdownOnSignals(ctx):
+ stop()
+ case <-quit:
+ stop()
+ }
}
// Puts key and value into the syncbase table
@@ -94,6 +98,7 @@
fmt.Println("TABLE ERROR: ", err)
}
}
+ // Add user settings data to represent this player
settingsMap := make(map[string]interface{})
settingsMap["userID"] = util.UserID
settingsMap["avatar"] = util.UserAvatar
@@ -106,9 +111,12 @@
settingsTable.Put(u.Ctx, fmt.Sprintf("users/%d/settings", util.UserID), value)
}
-// Creates a new syncgroup
-func CreateSyncgroup(ch chan string, u *uistate.UIState) {
+// Creates a new set of gamelog and game settings syncgroups
+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{})
gameMap["type"] = "Hearts"
@@ -119,7 +127,8 @@
if err != nil {
fmt.Println("WE HAVE A HUGE PROBLEM:", err)
}
- sgName := util.MountPoint + "/croupier/" + util.SBName + "/%%sync/gaming-" + string(value)
+ // Create gamelog syncgroup
+ logSGName := util.MountPoint + "/croupier/" + util.SBName + "/%%sync/gaming-" + string(value)
allAccess := access.AccessList{In: []security.BlessingPattern{"..."}}
permissions := access.Permissions{
"Admin": allAccess,
@@ -128,63 +137,47 @@
"Resolve": allAccess,
"Debug": allAccess,
}
- pref := wire.TableRow{util.LogName, ""}
- prefs := []wire.TableRow{pref}
+ logPref := wire.TableRow{util.LogName, ""}
+ logPrefs := []wire.TableRow{logPref}
tables := []string{util.MountPoint + "/croupier"}
- spec := wire.SyncgroupSpec{
+ logSpec := wire.SyncgroupSpec{
Description: "croupier syncgroup",
Perms: permissions,
- Prefixes: prefs,
+ Prefixes: logPrefs,
MountTables: tables,
IsPrivate: false,
}
myInfoCreator := wire.SyncgroupMemberInfo{8, true}
app := u.Service.App(util.AppName)
db := app.NoSQLDatabase(util.DbName, nil)
- sg := db.Syncgroup(sgName)
- err = sg.Create(u.Ctx, spec, myInfoCreator)
+ logSG := db.Syncgroup(logSGName)
+ err = logSG.Create(u.Ctx, logSpec, myInfoCreator)
if err != nil {
fmt.Println("SYNCGROUP CREATE ERROR: ", err)
ch <- ""
} else {
fmt.Println("Syncgroup created")
u.GameID = gameID
- ch <- sgName
+ ch <- logSGName
}
-}
-
-// Creates a new syncgroup
-func CreateSettingsSyncgroup(ch chan string, u *uistate.UIState) {
- fmt.Println("Creating Settings Syncgroup")
- sgName := fmt.Sprintf("%s/croupier/%s/%%%%sync/discovery-%d", util.MountPoint, util.SBName, util.UserID)
- allAccess := access.AccessList{In: []security.BlessingPattern{"..."}}
- permissions := access.Permissions{
- "Admin": allAccess,
- "Write": allAccess,
- "Read": allAccess,
- "Resolve": allAccess,
- "Debug": allAccess,
- }
- pref := wire.TableRow{util.SettingsName, ""}
- prefs := []wire.TableRow{pref}
- tables := []string{util.MountPoint + "/croupier"}
- spec := wire.SyncgroupSpec{
+ // Create game settings syncgroup
+ settingsSGName := fmt.Sprintf("%s/croupier/%s/%%%%sync/discovery-%d", util.MountPoint, util.SBName, util.UserID)
+ settingsPref := wire.TableRow{util.SettingsName, ""}
+ settingsPrefs := []wire.TableRow{settingsPref}
+ settingsSpec := wire.SyncgroupSpec{
Description: "croupier syncgroup",
Perms: permissions,
- Prefixes: prefs,
+ Prefixes: settingsPrefs,
MountTables: tables,
IsPrivate: false,
}
- myInfoCreator := wire.SyncgroupMemberInfo{8, true}
- app := u.Service.App(util.AppName)
- db := app.NoSQLDatabase(util.DbName, nil)
- sg := db.Syncgroup(sgName)
- err := sg.Create(u.Ctx, spec, myInfoCreator)
+ settingsSG := db.Syncgroup(settingsSGName)
+ err = settingsSG.Create(u.Ctx, settingsSpec, myInfoCreator)
if err != nil {
fmt.Println("SYNCGROUP CREATE ERROR: ", err)
ch <- ""
} else {
fmt.Println("Syncgroup created")
- ch <- sgName
+ ch <- settingsSGName
}
}
diff --git a/go/src/hearts/syncbase/util/util.go b/go/src/hearts/syncbase/util/util.go
index 9581f90..cb293cd 100644
--- a/go/src/hearts/syncbase/util/util.go
+++ b/go/src/hearts/syncbase/util/util.go
@@ -8,16 +8,16 @@
const (
// switch back to my mountpoint with the following code:
- // MountPoint = "users/emshack@google.com"
- MountPoint = "/192.168.86.254:8101"
+ MountPoint = "users/emshack@google.com"
+ //MountPoint = "/192.168.86.254:8101"
UserID = 123
UserColor = 16777215
UserAvatar = "Club.png"
UserName = "Croupier Go Player (the blue)"
- SBName = "syncbase1"
+ SBName = "syncbase"
AppName = "app"
DbName = "db"
LogName = "games"
SettingsName = "table_settings"
- CroupierInterface = "CroupierSettingsAndGame"
+ CroupierInterface = "CroupierSettingsAndGameEmily"
)
diff --git a/go/src/hearts/syncbase/watch/watch.go b/go/src/hearts/syncbase/watch/watch.go
index e2fc0ad..9d48658 100644
--- a/go/src/hearts/syncbase/watch/watch.go
+++ b/go/src/hearts/syncbase/watch/watch.go
@@ -24,26 +24,6 @@
"v.io/v23/syncbase/nosql"
)
-func PrintStream(prefix string, u *uistate.UIState) {
- stream, err := client.WatchData(u)
- if err != nil {
- fmt.Println("WatchData error:", err)
- }
- for {
- if updateExists := stream.Advance(); updateExists {
- c := stream.Change()
- if c.ChangeType == nosql.PutChange {
- var value []byte
- if err := c.Value(&value); err != nil {
- fmt.Println("Value error:", err)
- }
- valueStr := string(value)
- fmt.Println(prefix, valueStr)
- }
- }
- }
-}
-
func Update(u *uistate.UIState) {
stream, err := client.WatchData(u)
if err != nil {
@@ -62,15 +42,15 @@
updateType := strings.Split(valueStr, "|")[0]
switch updateType {
case gamelog.Deal:
- onDeal(valueStr, u)
+ go onDeal(valueStr, u)
case gamelog.Pass:
- onPass(valueStr, u)
+ go onPass(valueStr, u)
case gamelog.Take:
- onTake(valueStr, u)
+ go onTake(valueStr, u)
case gamelog.Play:
- onPlay(valueStr, u)
+ go onPlay(valueStr, u)
case gamelog.Ready:
- onReady(valueStr, u)
+ go onReady(valueStr, u)
}
} else {
fmt.Println("Unexpected ChangeType: ", c.ChangeType)
@@ -85,7 +65,11 @@
u.CurTable.GetPlayers()[playerInt].SetHand(curCards)
if u.CurTable.AllDoneDealing() {
u.CurTable.NewRound()
- view.LoadTableView(u)
+ if u.CurPlayerIndex >= 0 && u.CurPlayerIndex < u.NumPlayers {
+ view.LoadPassOrTakeOrPlay(u)
+ } else {
+ view.LoadTableView(u)
+ }
}
}
@@ -104,15 +88,14 @@
for _, c := range curCards {
u.CurTable.GetPlayers()[playerInt].RemoveFromHand(c)
}
- if u.CurPlayerIndex >= 0 {
- u.Cards = u.CurTable.GetPlayers()[u.CurPlayerIndex].GetHand()
- }
u.CurTable.GetPlayers()[playerInt].SetPassedFrom(curCards)
u.CurTable.GetPlayers()[receivingPlayer].SetPassedTo(curCards)
u.CurTable.GetPlayers()[playerInt].SetDonePassing(true)
// UI
if u.CurView == uistate.Table {
- reposition.AnimateTableCardPass(curCards, receivingPlayer, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
+ reposition.AnimateTableCardPass(curCards, receivingPlayer, quit, u)
reposition.SetTableDropColors(u)
} else if u.CurView == uistate.Take && u.CurPlayerIndex == receivingPlayer {
view.LoadTakeView(u)
@@ -130,9 +113,6 @@
p.AddToHand(c)
}
u.CurTable.GetPlayers()[playerInt].SetDoneTaking(true)
- if u.CurPlayerIndex >= 0 {
- u.Cards = u.CurTable.GetPlayers()[u.CurPlayerIndex].GetHand()
- }
if p.HasTwoOfClubs() {
u.CurTable.SetFirstPlayer(p.GetPlayerIndex())
// UI
@@ -142,7 +122,9 @@
}
// UI
if u.CurView == uistate.Table {
- reposition.AnimateTableCardTake(passed, u.CurTable.GetPlayers()[playerInt], u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
+ reposition.AnimateTableCardTake(passed, u.CurTable.GetPlayers()[playerInt], quit, u)
reposition.SetTableDropColors(u)
}
}
@@ -154,9 +136,6 @@
u.CurTable.GetPlayers()[playerInt].RemoveFromHand(playedCard)
u.CurTable.SetPlayedCard(playedCard, playerInt)
u.CurTable.GetPlayers()[playerInt].SetDonePlaying(true)
- if u.CurPlayerIndex >= 0 {
- u.Cards = u.CurTable.GetPlayers()[u.CurPlayerIndex].GetHand()
- }
trickOver := true
trickCards := u.CurTable.GetTrick()
for _, c := range trickCards {
@@ -176,7 +155,9 @@
}
// UI
if u.CurView == uistate.Table {
- reposition.AnimateTableCardPlay(playedCard, playerInt, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
+ reposition.AnimateTableCardPlay(playedCard, playerInt, quit, u)
reposition.SetTableDropColors(u)
if trickOver {
var trickDir direction.Direction
@@ -190,28 +171,38 @@
case 3:
trickDir = direction.Right
}
- reposition.AnimateTableCardTakeTrick(trickCards, trickDir, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
+ reposition.AnimateTableCardTakeTrick(trickCards, trickDir, quit, u)
}
} else if u.CurView == uistate.Split {
- if playerInt != u.CurPlayerIndex {
- reposition.AnimateSplitCardPlay(playedCard, playerInt, u)
- }
- reposition.SetSplitDropColors(u)
- if trickOver {
- var trickDir direction.Direction
- switch recipient {
- case u.CurPlayerIndex:
- trickDir = direction.Down
- case (u.CurPlayerIndex + 1) % u.NumPlayers:
- trickDir = direction.Left
- case (u.CurPlayerIndex + 2) % u.NumPlayers:
- trickDir = direction.Across
- case (u.CurPlayerIndex + 3) % u.NumPlayers:
- trickDir = direction.Right
+ if roundOver {
+ view.LoadScoreView(roundScores, winners, u)
+ } else {
+ if playerInt != u.CurPlayerIndex {
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
+ reposition.AnimateSplitCardPlay(playedCard, playerInt, quit, u)
}
- reposition.AnimateTableCardTakeTrick(trickCards, trickDir, u)
+ reposition.SetSplitDropColors(u)
+ if trickOver {
+ var trickDir direction.Direction
+ switch recipient {
+ case u.CurPlayerIndex:
+ trickDir = direction.Down
+ case (u.CurPlayerIndex + 1) % u.NumPlayers:
+ trickDir = direction.Left
+ case (u.CurPlayerIndex + 2) % u.NumPlayers:
+ trickDir = direction.Across
+ case (u.CurPlayerIndex + 3) % u.NumPlayers:
+ trickDir = direction.Right
+ }
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
+ reposition.AnimateTableCardTakeTrick(trickCards, trickDir, quit, u)
+ }
+ view.LoadSplitView(true, u)
}
- view.LoadSplitView(true, u)
} else if u.CurView == uistate.Play {
if roundOver {
view.LoadScoreView(roundScores, winners, u)
@@ -240,16 +231,19 @@
// logic
playerInt, _ := parsePlayerAndCards(value, u)
u.CurTable.GetPlayers()[playerInt].SetDoneScoring(true)
- if u.CurTable.AllReadyForNewRound() {
+ if u.CurTable.AllReadyForNewRound() && u.IsOwner {
newHands := u.CurTable.Deal()
- success := gamelog.LogDeal(u, playerInt, newHands)
+ success := gamelog.LogDeal(u, u.CurPlayerIndex, newHands)
for !success {
- success = gamelog.LogDeal(u, playerInt, newHands)
+ success = gamelog.LogDeal(u, u.CurPlayerIndex, newHands)
}
}
// UI
- if playerInt == u.CurPlayerIndex {
- view.LoadTableView(u)
+ if u.CurTable.AllReadyForNewRound() {
+ if u.SGChan != nil {
+ u.SGChan <- true
+ u.SGChan = nil
+ }
}
}
diff --git a/go/src/hearts/touchhandler/touchhandler.go b/go/src/hearts/touchhandler/touchhandler.go
index 5adfe92..78b389b 100644
--- a/go/src/hearts/touchhandler/touchhandler.go
+++ b/go/src/hearts/touchhandler/touchhandler.go
@@ -28,10 +28,10 @@
case touch.TypeBegin:
beginClickDiscovery(t, u)
}
- case uistate.Opening:
+ case uistate.Arrange:
switch t.Type {
case touch.TypeBegin:
- beginClickOpening(t, u)
+ beginClickArrange(t, u)
}
case uistate.Table:
switch t.Type {
@@ -104,15 +104,16 @@
buttonList := findClickedButton(t, u)
if len(buttonList) > 0 {
if buttonList[0] == u.Buttons[0] {
- logChan := make(chan string)
- settingsChan := make(chan string)
- go server.CreateSyncgroup(logChan, u)
- go server.CreateSettingsSyncgroup(settingsChan, u)
- logName := <-logChan
- settingsName := <-settingsChan
+ ch := make(chan string)
+ go server.CreateSyncgroups(ch, u)
+ logName := <-ch
+ settingsName := <-ch
if logName != "" && settingsName != "" {
- go server.Advertise(logName, settingsName, u.Ctx)
- view.LoadOpeningView(u)
+ u.ScanChan <- true
+ u.ScanChan = nil
+ u.SGChan = make(chan bool)
+ go server.Advertise(logName, settingsName, u.SGChan, u.Ctx)
+ view.LoadArrangeView(u)
}
} else {
for _, b := range u.Buttons {
@@ -122,7 +123,9 @@
logAddr := b.GetInfo()[1]
go client.JoinSyncgroups(joinDone, logAddr, settingsAddr, u)
if success := <-joinDone; success {
- view.LoadOpeningView(u)
+ u.ScanChan <- true
+ u.ScanChan = nil
+ view.LoadArrangeView(u)
} else {
fmt.Println("Failed to join")
}
@@ -132,12 +135,11 @@
}
}
-func beginClickOpening(t touch.Event, u *uistate.UIState) {
+func beginClickArrange(t touch.Event, u *uistate.UIState) {
buttonList := findClickedButton(t, u)
if len(buttonList) > 0 {
- fmt.Println("Dealing")
- allHands := u.CurTable.Deal()
- gamelog.LogDeal(u, u.CurPlayerIndex, allHands)
+ gamelog.LogReady(u)
+ view.LoadWaitingView(u)
}
}
@@ -177,10 +179,18 @@
} else {
pullTab := u.Buttons[0]
if pullTab.GetDisplayingImage() {
+ u.CurImg = u.Buttons[0]
for _, img := range u.Other {
u.Eng.SetSubTex(img.GetNode(), img.GetAlt())
img.SetDisplayingImage(false)
}
+ blueBanner := u.Other[0]
+ if blueBanner.GetNode().Arranger == nil {
+ finalX := blueBanner.GetInitial().X
+ finalY := pullTab.GetInitial().Y + pullTab.GetDimensions().Y - blueBanner.GetDimensions().Y
+ finalPos := coords.MakeVec(finalX, finalY)
+ reposition.AnimateImageNoChannel(blueBanner, finalPos, blueBanner.GetDimensions(), u)
+ }
}
}
}
@@ -236,13 +246,17 @@
} else if u.CurImg != nil && touchingStaticImg(t, u.Other[0], u) {
ch := make(chan bool)
success := passCards(ch, u.CurPlayerIndex, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
go func() {
- <-ch
- if !success {
- fmt.Println("Invalid pass")
- } else {
- view.LoadTakeView(u)
+ onDone := func() {
+ if !success {
+ fmt.Println("Invalid pass")
+ } else {
+ view.LoadTakeView(u)
+ }
}
+ reposition.SwitchOnChan(ch, quit, onDone, u)
}()
}
u.CurCard = nil
@@ -251,6 +265,9 @@
func beginClickTake(t touch.Event, u *uistate.UIState) {
u.CurCard = findClickedCard(t, u)
+ if u.CurCard != nil {
+ u.CurCard.GetNode().Arranger = nil
+ }
buttonList := findClickedButton(t, u)
if len(buttonList) > 0 {
if u.Debug {
@@ -282,13 +299,17 @@
if doneTaking {
ch := make(chan bool)
success := takeCards(ch, u.CurPlayerIndex, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
go func() {
- <-ch
- if !success {
- fmt.Println("Invalid take")
- } else {
- view.LoadPlayView(u)
+ onDone := func() {
+ if !success {
+ fmt.Println("Invalid take")
+ } else {
+ view.LoadPlayView(u)
+ }
}
+ reposition.SwitchOnChan(ch, quit, onDone, u)
}()
}
}
@@ -306,6 +327,9 @@
} else if u.Buttons[2] == buttonList[0] {
view.LoadPassOrTakeOrPlay(u)
}
+ } else {
+ u.CurImg = u.Buttons[0]
+ view.LoadSplitView(false, u)
}
}
}
@@ -324,9 +348,11 @@
reposition.ResetCardPosition(u.CurCard, u.Eng)
reposition.RealignSuit(u.CurCard.GetSuit(), u.CurCard.GetInitial().Y, u)
}
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
go func() {
- <-ch
- view.LoadPlayView(u)
+ onDone := func() { view.LoadPlayView(u) }
+ reposition.SwitchOnChan(ch, quit, onDone, u)
}()
} else {
// check to see if card was removed from a drop target
@@ -345,9 +371,11 @@
if u.Buttons[0] == buttonList[0] {
ch := make(chan bool)
reposition.AnimateOutSplit(ch, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
go func() {
- <-ch
- view.LoadPlayView(u)
+ onDone := func() { view.LoadPlayView(u) }
+ reposition.SwitchOnChan(ch, quit, onDone, u)
}()
} else if u.Buttons[1] == buttonList[0] {
view.LoadTableView(u)
@@ -357,9 +385,11 @@
} else {
ch := make(chan bool)
reposition.AnimateOutSplit(ch, u)
+ quit := make(chan bool)
+ u.AnimChans = append(u.AnimChans, quit)
go func() {
- <-ch
- view.LoadPlayView(u)
+ onDone := func() { view.LoadPlayView(u) }
+ reposition.SwitchOnChan(ch, quit, onDone, u)
}()
}
}
@@ -397,6 +427,7 @@
for !success {
gamelog.LogReady(u)
}
+ view.LoadWaitingView(u)
}
}