package BingoManager import ( "context" "crypto/rand" "encoding/hex" "errors" "log" mathrand "math/rand" "strings" "sync" "time" "github.com/golang/protobuf/proto" "github.com/segmentio/kafka-go" glbingopb "git.alfi.li/gamelang/protobuf/gamelang-bingo" ) // BingoManager manages holds type BingoManager struct { Games map[string]*glbingopb.Bingo GamesLock sync.Mutex kafkaBroker []string kafkaR *kafka.Reader kafkaW *kafka.Writer notichan chan glbingopb.Event } // NewBingoManager returns an initialized BingoManager func NewBingoManager(kafkaBroker []string, notichan chan glbingopb.Event) *BingoManager { // default broker: []string{"localhost:9092"} bs := BingoManager{notichan: notichan} bs.Games = map[string]*glbingopb.Bingo{} if len(kafkaBroker) > 0 { bs.kafkaBroker = kafkaBroker bs.InitKafka() } go bs.CheckWin() return &bs } func (bs *BingoManager) CheckWin() { for { bs.GamesLock.Lock() games := bs.Games for _, game := range games { //log.Println("CheckWin: checking game", game) if strings.Split(game.Name, "\uffff")[1] == "lobby" { continue } win, winner := bs.CheckHorizontal(game) if !win { win, winner = bs.CheckVertical(game) } if win { game.Winner = winner game.Finished = true bs.notify(glbingopb.Event{Type: "Win", Gamename: game.Name, Username: winner}) } } bs.GamesLock.Unlock() time.Sleep(time.Second) } } func (bs *BingoManager) CheckHorizontal(game *glbingopb.Bingo) (bool, string) { log.Println("CheckHorizontal: checking game", game.Name) fields := game.GetFields() for _, row := range fields.GetRows() { //log.Println("CheckHorizontal: checking row", i) hits := uint32(0) winner := "" for _, field := range row.GetFields() { //log.Println("CheckHorizontal: checking field", y) if field.Checked { if winner == "" { winner = field.Checker[0] //log.Println("CheckHorizontal: new winner", winner) hits++ continue } if winner == field.Checker[0] { //log.Println("CheckHorizontal: same winner", winner) hits++ continue } //log.Println("CheckHorizontal: other winner", winner) break } //else { //log.Println("CheckHorizontal: not checked") //} } if hits == game.Numcols { //log.Println("got a winner", winner) return true, winner } } return false, "" } func (bs *BingoManager) CheckVertical(game *glbingopb.Bingo) (bool, string) { log.Println("CheckHorizontal: checking game", game.Name) fields := game.GetFields() hits := uint32(0) winner := "" for col := uint32(0); col < game.GetNumcols(); col++ { for _, row := range fields.GetRows() { field := row.Fields[col] if field.Checked { //log.Println("CheckHorizontal: checking field", y) if field.Checked { if winner == "" { winner = field.Checker[0] //log.Println("CheckHorizontal: new winner", winner) hits++ continue } if winner == field.Checker[0] { //log.Println("CheckHorizontal: same winner", winner) hits++ continue } //log.Println("CheckHorizontal: other winner", winner) break } } } } if hits == game.Numrows { //log.Println("got a winner", winner) return true, winner } return false, "" } // CraftGame crafts a new Game func (bs *BingoManager) CraftGame(id uint32, name string, wordlist []string, numrows, numcols uint32, options ...*glbingopb.Option) glbingopb.Bingo { log.Println("crafting game", name) newGame := glbingopb.Bingo{Id: id, Name: name, Textlist: wordlist, Options: options, Numrows: numrows, Numcols: numcols} log.Println("new game:\n", newGame) matrix := glbingopb.FieldMatrix{Rows: []*glbingopb.FieldMatrix_MatrixRow{}} for row := uint32(0); row < numrows; row += 1 { matrix.Rows = append(matrix.Rows, &glbingopb.FieldMatrix_MatrixRow{}) matrix.Rows[row].Fields = []*glbingopb.Field{} for col := uint32(0); col < numrows; col += 1 { if getOption(options, "unique fields").Value == "true" { var word string word, wordlist = pickRandomEntryAndDelete(wordlist) matrix.Rows[row].Fields = append(matrix.Rows[row].Fields, &glbingopb.Field{Text: word}) } else { word, _ := pickRandomEntry(wordlist) matrix.Rows[row].Fields = append(matrix.Rows[row].Fields, &glbingopb.Field{Text: word}) } } } newGame.Fields = &matrix return newGame } func getOption(options []*glbingopb.Option, search string) *glbingopb.Option { for _, option := range options { if option.Key == search { return option } } return &glbingopb.Option{} } func pickRandomEntry(list []string) (string, int) { index := mathrand.Int() % len(list) return list[index], index } func pickRandomEntryAndDelete(list []string) (string, []string) { entry, index := pickRandomEntry(list) list[index] = list[len(list)-1] return entry, list[:len(list)-1] } // Create adds an entry into the Game database func (bs *BingoManager) Create(newGame glbingopb.Bingo) error { log.Println("BingoManager: create game", newGame.Name) if bs.kafkaW != nil { newGameBin, _ := proto.Marshal(&newGame) if err := bs.kafkaW.WriteMessages(context.Background(), kafka.Message{Key: []byte("Game:create"), Value: newGameBin}); err != nil { log.Println("ERROR: cannot write to kafka\n", err.Error()) return err } return nil } else { log.Println("creating Game:\n", newGame) bs._create(newGame) return nil } } func (bs *BingoManager) _create(newGame glbingopb.Bingo) { bs.GamesLock.Lock() bs.Games[newGame.Name] = &newGame bs.GamesLock.Unlock() log.Println("BingoManager: Game has been created", newGame) } // Remove removes an entry from the Game database func (bs *BingoManager) Remove(name string) error { if bs.kafkaW != nil { bs.GamesLock.Lock() delGame := bs.Games[name] bs.GamesLock.Unlock() delGameBin, _ := proto.Marshal(delGame) if err := bs.kafkaW.WriteMessages(context.Background(), kafka.Message{Key: []byte("Game:delete"), Value: delGameBin}); err != nil { log.Fatal(err) } else { // print common key info log.Print("Delete is done") } } else { bs.GamesLock.Lock() delete(bs.Games, name) bs.GamesLock.Unlock() } return nil } // Get returns an Instance of a Game func (bs *BingoManager) Get(searchGame glbingopb.Bingo) glbingopb.Bingo { bs.GamesLock.Lock() Game, ok := bs.Games[searchGame.Name] bs.GamesLock.Unlock() if !ok { log.Println("BingoManager: cannot find Game", searchGame.Name) return glbingopb.Bingo{} } return *Game } // List returns a list of all Games func (bs *BingoManager) List() []glbingopb.Bingo { list := []glbingopb.Bingo{} bs.GamesLock.Lock() games := bs.Games for _, Game := range games { list = append(list, *Game) } bs.GamesLock.Unlock() return list } // Modify adapts the changeset in modifyGame func (bs *BingoManager) Modify(modGame glbingopb.Bingo) error { if bs.kafkaW != nil { modGameBin, _ := proto.Marshal(&modGame) if err := bs.kafkaW.WriteMessages(context.Background(), kafka.Message{Key: []byte("Game:modify"), Value: modGameBin}); err != nil { log.Fatal(err) return err } else { log.Print("modify sent") return nil } } else { return bs._modifyGame(modGame) } } // modifyGame _actually_ applies the diff to the live game func (bs *BingoManager) _modifyGame(modGame glbingopb.Bingo) error { bs.GamesLock.Lock() Game, ok := bs.Games[modGame.Name] bs.GamesLock.Unlock() if !ok { log.Printf("BingoManager: Game \"%v\" not found", modGame.Name) return errors.New("Game not found") } // do nothing if game is won by someone if Game.Finished { return errors.New("game is already finished") } bs.GamesLock.Lock() bs.Games[modGame.Name] = &modGame bs.GamesLock.Unlock() return nil } // HandleEvent handles incoming game events func (bs *BingoManager) HandleEvent(event glbingopb.Event) error { bs.GamesLock.Lock() game, ok := bs.Games[event.Gamename] if game.Finished { return errors.New("game finished") } if !ok { return errors.New("game not found") } bs.GamesLock.Unlock() if bs.kafkaW != nil { eventBin, _ := proto.Marshal(&event) if err := bs.kafkaW.WriteMessages(context.Background(), kafka.Message{Key: []byte("Game:event"), Value: eventBin}); err != nil { log.Fatal(err) return err } else { log.Print("event sent") return nil } } else { return bs._handleEvent(event) } } // HandleEvent _actually_ handles incoming game events func (bs *BingoManager) _handleEvent(event glbingopb.Event) error { log.Println("BingoManager: handling event", event) // if this is not a common game, all other games from the world should also receive the event neighbors := event.GetNeighborgames() if len(neighbors) > 0 { for _, game := range neighbors { event.Gamename = game event.Neighborgames = []string{} log.Println("BingoManager: trigger event for", game, "with", event) go bs._handleEvent(event) } } if event.Type == "checkField" { bs.GamesLock.Lock() game, ok := bs.Games[event.Gamename] bs.GamesLock.Unlock() if !ok { log.Println("BingoManager: cannot find Game to check", event.Gamename) return errors.New("cannot find Game") } checkedgame := bs.checkField(event.Value, *game, event.Username) bs.GamesLock.Lock() bs.Games[event.Gamename] = &checkedgame bs.GamesLock.Unlock() // send the event out to wss clients go bs.notify(event) return nil } return errors.New("unknown event type") } func (bs *BingoManager) checkField(text string, game glbingopb.Bingo, checker string) glbingopb.Bingo { matrix := game.GetFields() for _, row := range matrix.GetRows() { for _, field := range row.Fields { if field.Text == text { field.Checked = true field.Checker = append(field.Checker, checker) } } } //game.Fields = matrix return game } func (bs *BingoManager) notify(event glbingopb.Event) { log.Println("BingoManager: notify", event) bs.notichan <- event log.Print("BingoManager: notify sent") } // ReceiveMessages handles kafka messages func (bs *BingoManager) ReceiveMessages(msg kafka.Message) { log.Println("bs receive: new msg", msg.Offset, string(msg.Key)) switch string(msg.Key) { // uc - Game create event case "Game:create": newGame := &glbingopb.Bingo{} proto.Unmarshal(msg.Value, newGame) bs._create(*newGame) log.Println("bs receive: Game \"", newGame.Name, "\" has been created") // ur - Game remove event case "Game:remove": delGame := &glbingopb.Bingo{} proto.Unmarshal(msg.Value, delGame) name := delGame.Name bs.GamesLock.Lock() delete(bs.Games, name) bs.GamesLock.Unlock() log.Println("bs receive: Game \"", delGame.Name, "\" has been deleted") case "Game:modify": modGame := &glbingopb.Bingo{} proto.Unmarshal(msg.Value, modGame) bs._modifyGame(*modGame) case "Game:event": event := &glbingopb.Event{} proto.Unmarshal(msg.Value, event) bs._handleEvent(*event) default: log.Println("unknown type ", string(msg.Key)) } } // InitKafka initializes the kafka reader and writer and starts the watch thread func (bs *BingoManager) InitKafka() error { bs.kafkaR = kafka.NewReader(kafka.ReaderConfig{ Brokers: bs.kafkaBroker, Topic: "BingoManager", Partition: 0, MinBytes: 10e3, // 10KB MaxBytes: 10e6, // 10MB }) bs.kafkaW = kafka.NewWriter(kafka.WriterConfig{ Brokers: bs.kafkaBroker, Topic: "BingoManager", Balancer: &kafka.LeastBytes{}, }) go bs.pollKafka() return nil } func (bs *BingoManager) pollKafka() error { for { m, err := bs.kafkaR.ReadMessage(context.Background()) if err != nil { log.Print(err) time.Sleep(time.Second) continue } bs.ReceiveMessages(m) } } func randomHex(n int) (string, error) { bytes := make([]byte, n) if _, err := rand.Read(bytes); err != nil { return "", err } return hex.EncodeToString(bytes), nil }