| // 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. |
| |
| // watch.go holds all code to handle updates to the syncbase gamelog. |
| // Update() is to be run as a goroutine, getting a watchstream from |
| // the syncgroup and updating the game state and UI display as a result |
| // of any changes that come along. |
| |
| package sync |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "golang.org/x/mobile/exp/sprite" |
| "hearts/img/direction" |
| "hearts/img/reposition" |
| "hearts/img/uistate" |
| "hearts/img/view" |
| "hearts/logic/card" |
| "os" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| "v.io/v23/syncbase/nosql" |
| ) |
| |
| func UpdateSettings(u *uistate.UIState) { |
| scanner := ScanData(SettingsName, "users", u) |
| for { |
| if updateExists := scanner.Advance(); updateExists { |
| key := scanner.Key() |
| var value []byte |
| if err := scanner.Value(&value); err != nil { |
| fmt.Println("Value error:", err) |
| } |
| handleSettingsUpdate(key, value, u) |
| } else { |
| break |
| } |
| } |
| stream, err := WatchData(SettingsName, "users", u) |
| if err != nil { |
| fmt.Println("WatchData error:", err) |
| } else { |
| 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) |
| } |
| handleSettingsUpdate(key, value, u) |
| } else { |
| fmt.Println("Unexpected ChangeType: ", c.ChangeType) |
| } |
| } |
| } |
| } |
| } |
| |
| func handleSettingsUpdate(key string, value []byte, u *uistate.UIState) { |
| var valueMap map[string]interface{} |
| err := json.Unmarshal(value, &valueMap) |
| if err != nil { |
| fmt.Println("Unmarshal error:", err) |
| } |
| userID, _ := strconv.Atoi(strings.Split(key, "/")[1]) |
| u.UserData[userID] = valueMap |
| for _, v := range u.PlayerData { |
| if v == userID { |
| view.ReloadView(u) |
| } |
| } |
| if u.CurView == uistate.Discovery { |
| view.LoadDiscoveryView(u) |
| } |
| } |
| |
| func UpdateGame(quit chan bool, u *uistate.UIState) { |
| file, err := os.OpenFile("/sdcard/test.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) |
| if err != nil { |
| fmt.Println("err:", err) |
| } |
| fmt.Fprintf(file, fmt.Sprintf("\n***NEW GAME: %d\n", u.GameID)) |
| defer file.Close() |
| scanner := ScanData(LogName, fmt.Sprintf("%d", u.GameID), u) |
| m := make(map[string][]byte) |
| keys := make([]string, 0) |
| for scanner.Advance() { |
| k := scanner.Key() |
| var v []byte |
| if err := scanner.Value(&v); err != nil { |
| fmt.Println("Value error:", err) |
| } |
| id := strings.Split(k, "/")[0] |
| if id == fmt.Sprintf("%d", u.GameID) { |
| m[k] = v |
| keys = append(keys, k) |
| } |
| } |
| sort.Sort(scanSorter(keys)) |
| for _, key := range keys { |
| select { |
| case <-quit: |
| return |
| default: |
| value := m[key] |
| handleGameUpdate(file, key, value, u) |
| } |
| } |
| stream, err2 := WatchData(LogName, fmt.Sprintf("%d", u.GameID), u) |
| fmt.Println("STARTING WATCH FOR GAME", u.GameID) |
| if err2 != nil { |
| fmt.Println("WatchData error:", err2) |
| } |
| updateBlock := make([]nosql.WatchChange, 0) |
| for { |
| if updateExists := stream.Advance(); updateExists { |
| c := stream.Change() |
| updateBlock = append(updateBlock, c) |
| if !c.Continued { |
| sort.Sort(updateSorter(updateBlock)) |
| for _, c := range updateBlock { |
| select { |
| case <-quit: |
| return |
| default: |
| if c.ChangeType == nosql.PutChange { |
| key := c.Row |
| var value []byte |
| if err := c.Value(&value); err != nil { |
| fmt.Println("Value error:", err) |
| } |
| handleGameUpdate(file, key, value, u) |
| } else { |
| fmt.Println("Unexpected ChangeType: ", c.ChangeType) |
| } |
| } |
| } |
| updateBlock = make([]nosql.WatchChange, 0) |
| } |
| } |
| } |
| } |
| |
| func handleGameUpdate(file *os.File, key string, value []byte, u *uistate.UIState) { |
| curTime := time.Now().UnixNano() / 1000000 |
| valueStr := string(value) |
| fmt.Fprintf(file, fmt.Sprintf("key: %s\n", key)) |
| fmt.Fprintf(file, fmt.Sprintf("value: %s\n", valueStr)) |
| fmt.Fprintf(file, fmt.Sprintf("time: %v\n", curTime)) |
| tmp := strings.Split(key, "/") |
| if len(tmp) == 3 { |
| keyTime, _ := strconv.ParseInt(strings.Split(tmp[2], "-")[0], 10, 64) |
| fmt.Fprintf(file, fmt.Sprintf("diff: %d milliseconds\n\n", curTime-keyTime)) |
| } else { |
| fmt.Fprintf(file, "\n") |
| } |
| fmt.Println(key, valueStr) |
| keyType := strings.Split(key, "/")[1] |
| switch keyType { |
| case "log": |
| updateType := strings.Split(valueStr, "|")[0] |
| switch updateType { |
| case Deal: |
| onDeal(valueStr, u) |
| case Pass: |
| onPass(valueStr, u) |
| case Take: |
| onTake(valueStr, u) |
| case Play: |
| onPlay(valueStr, u) |
| case TakeTrick: |
| onTakeTrick(valueStr, u) |
| case Ready: |
| onReady(valueStr, u) |
| } |
| case "players": |
| switch strings.Split(key, "/")[3] { |
| case "player_number": |
| onPlayerNum(key, valueStr, u) |
| case "settings_sg": |
| onSettings(key, valueStr, u) |
| } |
| } |
| } |
| |
| func onPlayerNum(key, value string, u *uistate.UIState) { |
| userID, _ := strconv.Atoi(strings.Split(key, "/")[2]) |
| playerNum, _ := strconv.Atoi(value) |
| if playerNum >= 0 && playerNum < 4 { |
| u.PlayerData[playerNum] = userID |
| u.CurTable.GetPlayers()[playerNum].SetDoneScoring(true) |
| } |
| if playerNum == u.CurPlayerIndex && userID != UserID { |
| u.CurPlayerIndex = -1 |
| } |
| if u.CurView == uistate.Arrange { |
| view.LoadArrangeView(u) |
| if u.CurTable.AllReadyForNewRound() && u.IsOwner { |
| b := u.Buttons["start"] |
| u.Eng.SetSubTex(b.GetNode(), b.GetImage()) |
| b.SetDisplayingImage(true) |
| if u.SGChan != nil { |
| u.SGChan <- true |
| u.SGChan = nil |
| } |
| } |
| } |
| } |
| |
| func onSettings(key, value string, u *uistate.UIState) { |
| JoinSettingsSyncgroup(value, u) |
| } |
| |
| func onDeal(value string, u *uistate.UIState) { |
| playerInt, curCards := parsePlayerAndCards(value, u) |
| u.CurTable.GetPlayers()[playerInt].SetHand(curCards) |
| if u.CurTable.AllDoneDealing() { |
| u.CurTable.NewRound() |
| if u.CurPlayerIndex >= 0 && u.CurPlayerIndex < u.NumPlayers { |
| view.LoadPassOrTakeOrPlay(u) |
| } else { |
| view.LoadTableView(u) |
| } |
| } |
| } |
| |
| func onPass(value string, u *uistate.UIState) { |
| // logic |
| playerInt, curCards := parsePlayerAndCards(value, u) |
| var receivingPlayer int |
| switch u.CurTable.GetDir() { |
| case direction.Right: |
| receivingPlayer = (playerInt + 3) % u.NumPlayers |
| case direction.Left: |
| receivingPlayer = (playerInt + 1) % u.NumPlayers |
| case direction.Across: |
| receivingPlayer = (playerInt + 2) % u.NumPlayers |
| } |
| for _, c := range curCards { |
| u.CurTable.GetPlayers()[playerInt].RemoveFromHand(c) |
| } |
| u.CurTable.GetPlayers()[playerInt].SetPassedFrom(curCards) |
| u.CurTable.GetPlayers()[receivingPlayer].SetPassedTo(curCards) |
| u.CurTable.GetPlayers()[playerInt].SetDonePassing(true) |
| // UI |
| if u.CurView == uistate.Table { |
| quit := make(chan bool) |
| u.AnimChans = append(u.AnimChans, quit) |
| reposition.AnimateTableCardPass(curCards, receivingPlayer, quit, u) |
| view.LoadTableView(u) |
| } else if u.CurView == uistate.Take { |
| if u.SequentialPhases { |
| if u.CurTable.AllDonePassing() { |
| view.LoadTakeView(u) |
| } |
| } else if u.CurPlayerIndex == receivingPlayer { |
| view.LoadTakeView(u) |
| } |
| } else if u.CurView == uistate.Play && u.CurTable.AllDonePassing() { |
| view.LoadPlayView(true, u) |
| } |
| } |
| |
| func onTake(value string, u *uistate.UIState) { |
| // logic |
| playerInt, _ := parsePlayerAndCards(value, u) |
| p := u.CurTable.GetPlayers()[playerInt] |
| passed := p.GetPassedTo() |
| for _, c := range passed { |
| p.AddToHand(c) |
| } |
| u.CurTable.GetPlayers()[playerInt].SetDoneTaking(true) |
| if u.SequentialPhases { |
| if u.CurTable.AllDoneTaking() { |
| for _, player := range u.CurTable.GetPlayers() { |
| if player.HasTwoOfClubs() { |
| u.CurTable.SetFirstPlayer(player.GetPlayerIndex()) |
| } |
| } |
| // UI |
| if u.CurView == uistate.Play { |
| view.LoadPlayView(true, u) |
| } |
| } |
| } else if p.HasTwoOfClubs() { |
| u.CurTable.SetFirstPlayer(p.GetPlayerIndex()) |
| // UI |
| if u.CurView == uistate.Play && u.CurPlayerIndex != playerInt { |
| view.LoadPlayView(true, u) |
| } |
| } |
| // UI |
| if u.CurView == uistate.Table { |
| quit := make(chan bool) |
| u.AnimChans = append(u.AnimChans, quit) |
| reposition.AnimateTableCardTake(passed, u.CurTable.GetPlayers()[playerInt], quit, u) |
| view.LoadTableView(u) |
| } |
| } |
| |
| func onPlay(value string, u *uistate.UIState) { |
| // logic |
| playerInt, curCards := parsePlayerAndCards(value, u) |
| playedCard := curCards[0] |
| u.CurTable.GetPlayers()[playerInt].RemoveFromHand(playedCard) |
| u.CurTable.SetPlayedCard(playedCard, playerInt) |
| u.CurTable.GetPlayers()[playerInt].SetDonePlaying(true) |
| trickOver := u.CurTable.TrickOver() |
| var recipient int |
| if trickOver { |
| recipient = u.CurTable.GetTrickRecipient() |
| } |
| // UI |
| if u.CurView == uistate.Table { |
| quit := make(chan bool) |
| u.AnimChans = append(u.AnimChans, quit) |
| reposition.AnimateTableCardPlay(playedCard, playerInt, quit, u) |
| reposition.SetTableDropColors(u) |
| if trickOver { |
| // display take trick button |
| b := u.Buttons["takeTrick"] |
| u.Eng.SetSubTex(b.GetNode(), b.GetImage()) |
| } |
| } else if u.CurView == uistate.Split { |
| if playerInt != u.CurPlayerIndex { |
| quit := make(chan bool) |
| u.AnimChans = append(u.AnimChans, quit) |
| reposition.AnimateSplitCardPlay(playedCard, playerInt, quit, u) |
| } |
| reposition.SetSplitDropColors(u) |
| view.LoadSplitView(true, u) |
| if trickOver { |
| if recipient == u.CurPlayerIndex { |
| // display take trick button |
| b := u.Buttons["takeTrick"] |
| u.Eng.SetSubTex(b.GetNode(), b.GetImage()) |
| } |
| } else if u.CardToPlay != nil && u.CurTable.WhoseTurn() == u.CurPlayerIndex { |
| ch := make(chan bool) |
| if err := PlayCard(ch, u.CurPlayerIndex, u); err != "" { |
| view.ChangePlayMessage(err, u) |
| RemoveCardFromTarget(u.CardToPlay, u) |
| // add card back to hand |
| reposition.ResetCardPosition(u.CardToPlay, u.Eng) |
| } |
| u.CardToPlay = nil |
| u.BackgroundImgs[0].GetNode().Arranger = nil |
| var emptyTex sprite.SubTex |
| u.Eng.SetSubTex(u.BackgroundImgs[0].GetNode(), emptyTex) |
| } |
| } else if u.CurView == uistate.Play && u.CurPlayerIndex != playerInt { |
| view.LoadPlayView(true, u) |
| if u.CardToPlay != nil && u.CurTable.WhoseTurn() == u.CurPlayerIndex { |
| ch := make(chan bool) |
| if err := PlayCard(ch, u.CurPlayerIndex, u); err != "" { |
| view.ChangePlayMessage(err, u) |
| RemoveCardFromTarget(u.CardToPlay, u) |
| // add card back to hand |
| reposition.ResetCardPosition(u.CardToPlay, u.Eng) |
| reposition.RealignSuit(u.CardToPlay.GetSuit(), u.CardToPlay.GetInitial().Y, u) |
| } |
| u.CardToPlay = nil |
| quit := make(chan bool) |
| u.AnimChans = append(u.AnimChans, quit) |
| go func() { |
| onDone := func() { |
| if u.CurView == uistate.Play { |
| view.LoadPlayView(true, u) |
| } |
| } |
| reposition.SwitchOnChan(ch, quit, onDone, u) |
| }() |
| } |
| } |
| } |
| |
| func onTakeTrick(value string, u *uistate.UIState) { |
| trickCards := u.CurTable.GetTrick() |
| recipient := u.CurTable.GetTrickRecipient() |
| roundOver := u.CurTable.SendTrick(recipient) |
| if roundOver { |
| u.RoundScores, u.Winners = u.CurTable.EndRound() |
| } |
| // UI |
| if u.CurView == uistate.Table { |
| var emptyTex sprite.SubTex |
| u.Eng.SetSubTex(u.Buttons["takeTrick"].GetNode(), emptyTex) |
| var trickDir direction.Direction |
| switch recipient { |
| case 0: |
| trickDir = direction.Down |
| case 1: |
| trickDir = direction.Left |
| case 2: |
| trickDir = direction.Across |
| case 3: |
| trickDir = direction.Right |
| } |
| quit := make(chan bool) |
| u.AnimChans = append(u.AnimChans, quit) |
| reposition.AnimateTableCardTakeTrick(trickCards, trickDir, quit, u) |
| reposition.SetTableDropColors(u) |
| view.SetNumTricksTable(u) |
| } else if u.CurView == uistate.Split { |
| var emptyTex sprite.SubTex |
| u.Eng.SetSubTex(u.Buttons["takeTrick"].GetNode(), emptyTex) |
| if roundOver { |
| view.LoadScoreView(u) |
| } else { |
| 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) |
| } |
| } else if u.CurView == uistate.Play { |
| if roundOver { |
| view.LoadScoreView(u) |
| } else { |
| view.LoadPlayView(true, u) |
| } |
| } |
| // logic |
| if len(u.Winners) > 0 { |
| u.CurTable.NewGame() |
| } |
| } |
| |
| func onReady(value string, u *uistate.UIState) { |
| // logic |
| playerInt, _ := parsePlayerAndCards(value, u) |
| u.CurTable.GetPlayers()[playerInt].SetDoneScoring(true) |
| // UI |
| if u.CurTable.AllReadyForNewRound() && u.IsOwner { |
| if u.CurView == uistate.Arrange { |
| b := u.Buttons["start"] |
| u.Eng.SetSubTex(b.GetNode(), b.GetImage()) |
| b.SetDisplayingImage(true) |
| if u.SGChan != nil { |
| u.SGChan <- true |
| u.SGChan = nil |
| } |
| } else if u.CurView == uistate.Score { |
| newHands := u.CurTable.Deal() |
| successDeal := LogDeal(u, u.CurPlayerIndex, newHands) |
| for !successDeal { |
| successDeal = LogDeal(u, u.CurPlayerIndex, newHands) |
| } |
| } |
| } |
| } |
| |
| func parsePlayerAndCards(value string, u *uistate.UIState) (int, []*card.Card) { |
| updateContents := strings.Split(value, "|")[1] |
| playerIntPlusCards := strings.Split(updateContents, ":") |
| playerInt, _ := strconv.Atoi(playerIntPlusCards[0]) |
| cardList := u.CurTable.GetAllCards() |
| curCards := make([]*card.Card, 0) |
| for i := 1; i < len(playerIntPlusCards)-1; i++ { |
| cardInfo := playerIntPlusCards[i] |
| cardSuitFace := strings.Split(cardInfo, " ")[1] |
| cardSuit := card.ConvertToSuit(string(cardSuitFace[0])) |
| cardFace := card.ConvertToFace(string(cardSuitFace[1:])) |
| cardIndex := int(cardSuit*13) + int(cardFace) - 2 |
| curCards = append(curCards, cardList[cardIndex]) |
| } |
| return playerInt, curCards |
| } |
| |
| // Used to sort an array of watch changes |
| type updateSorter []nosql.WatchChange |
| |
| // Returns the length of the array |
| func (us updateSorter) Len() int { |
| return len(us) |
| } |
| |
| // Swaps the positions of two changes in the array |
| func (us updateSorter) Swap(i, j int) { |
| us[i], us[j] = us[j], us[i] |
| } |
| |
| // Compares two changes-- one card is less than another if it has an earlier timestamp |
| func (us updateSorter) Less(i, j int) bool { |
| iKey := us[i].Row |
| jKey := us[j].Row |
| return iKey < jKey |
| } |
| |
| type scanSorter []string |
| |
| func (ss scanSorter) Len() int { |
| return len(ss) |
| } |
| |
| // Swaps the positions of two changes in the array |
| func (ss scanSorter) Swap(i, j int) { |
| ss[i], ss[j] = ss[j], ss[i] |
| } |
| |
| // Compares two changes-- one card is less than another if it has an earlier timestamp |
| func (ss scanSorter) Less(i, j int) bool { |
| iKey := ss[i] |
| jKey := ss[j] |
| return iKey < jKey |
| } |
| |
| func PlayCard(ch chan bool, playerId int, u *uistate.UIState) string { |
| c := u.DropTargets[0].GetCardHere() |
| if c == nil { |
| return "No card has been played" |
| } |
| // checks to make sure that: |
| // -player has not already played a card this round |
| // -all players have passed cards |
| // -the play is in the right order |
| // -the play is valid given game logic |
| if u.CurTable.GetPlayers()[playerId].GetDonePlaying() { |
| return "You have already played a card in this trick" |
| } |
| if !u.CurTable.AllDonePassing() { |
| return "Not all players have passed their cards" |
| } |
| if !u.CurTable.ValidPlayOrder(playerId) { |
| return "It is not your turn" |
| } |
| if err := u.CurTable.ValidPlayLogic(c, playerId); err != "" { |
| return err |
| } |
| success := LogPlay(u, c) |
| for !success { |
| success = LogPlay(u, c) |
| } |
| // no animation when in split view |
| if u.CurView == uistate.Play { |
| reposition.AnimateHandCardPlay(ch, c, u) |
| } |
| return "" |
| } |
| |
| func RemoveCardFromTarget(c *card.Card, u *uistate.UIState) bool { |
| for _, d := range u.DropTargets { |
| if d.GetCardHere() == c { |
| d.SetCardHere(nil) |
| return true |
| } |
| } |
| return false |
| } |