diff --git a/dataBase.go b/dataBase.go index f4d628c..d5ae585 100644 --- a/dataBase.go +++ b/dataBase.go @@ -68,7 +68,11 @@ type Player struct { level int } -func fetchPlayer(r *sql.Rows, p *Player) error { +type scannable interface { + Scan(dest ...interface{}) error +} + +func fetchPlayer(r scannable, p *Player) error { var ( err error lastOnline string @@ -119,73 +123,56 @@ func (db *dbLayer) getPlayers(ctx context.Context) ([]Player, error) { func (db *dbLayer) getPlayerByID(ctx context.Context, id string) (*Player, error) { query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE id = $1" - rows, err := db.db.QueryContext(ctx, query, id) - if err != nil { + row := db.db.QueryRowContext(ctx, query, id) + if err := row.Err(); err != nil { return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) } - defer rows.Close() + var p Player - if rows.Next() { - var p Player - - err = fetchPlayer(rows, &p) - if err != nil { - return nil, err - } - - return &p, nil + err := fetchPlayer(row, &p) + if err != nil { + return nil, err } - return nil, nil + return &p, nil + } func (db *dbLayer) getPlayerByName(ctx context.Context, name string) (*Player, error) { query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE name = $1 LIMIT 1" - rows, err := db.db.QueryContext(ctx, query, name) - if err != nil { + row := db.db.QueryRowContext(ctx, query, name) + if err := row.Err(); err != nil { return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) } - defer rows.Close() + var p Player - if rows.Next() { - var p Player - - err = fetchPlayer(rows, &p) - if err != nil { - return nil, err - } - - return &p, nil + err := fetchPlayer(row, &p) + if err != nil { + return nil, err } - return nil, nil + return &p, nil } -func (db *dbLayer) getKillsByOPlayerID(ctx context.Context, playerID string) (int, error) { +func (db *dbLayer) getKillsByPlayerID(ctx context.Context, playerID string) (int, error) { query := "SELECT SUM(count) FROM killings WHERE player_id = $1 GROUP BY player_id" - rows, err := db.db.QueryContext(ctx, query, playerID) - if err != nil { + row := db.db.QueryRowContext(ctx, query, playerID) + if err := row.Err(); err != nil { return 0, errors.Wrapf(err, "Failed to run db query [%s]", query) } - defer rows.Close() + var killsNum int - if rows.Next() { - var killsNum int - - err = rows.Scan(&killsNum) - if err != nil { - return 0, errors.Wrap(err, "Failed to fetch data base") - } - - return killsNum, nil + err := row.Scan(&killsNum) + if err != nil { + return 0, errors.Wrap(err, "Failed to fetch data base") } - return 0, nil + return killsNum, nil } func (db *dbLayer) createPlayer(ctx context.Context, name string, lastOnline time.Time) (*Player, error) { diff --git a/go.mod b/go.mod index 93f6f89..ffbe44d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module mk-statistics +module craft-bot go 1.16 diff --git a/handlers.go b/handlers.go index 78a1469..336e901 100644 --- a/handlers.go +++ b/handlers.go @@ -49,7 +49,7 @@ func (c *joinCommand) run(ctx context.Context) { log := getLogger(ctx) pb := getDPlayersBoard(ctx) - mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s вошел в матрицу", c.name)) + mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s подключился", c.name)) m, err := botApi.Send(mess) if err != nil { log.Error(err) @@ -77,7 +77,9 @@ func (c *joinCommand) run(ctx context.Context) { } } - err = pb.getPlayerBoard(p.id).setOnline(true).updatePlayerInfo(ctx) + pb.getPlayerBoard(p.id).setOnline(true) + + err = pb.update(ctx, achievementVeryLongOnline) if err != nil { log.Error(err) } @@ -95,7 +97,7 @@ func (c *quitCommand) run(ctx context.Context) { log := getLogger(ctx) pb := getDPlayersBoard(ctx) - mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s покинул матрицу", c.name)) + mess := tgbotapi.NewMessage(cfg.ChatID, fmt.Sprintf("%s отключился", c.name)) m, err := botApi.Send(mess) if err != nil { log.Error(err) @@ -115,7 +117,9 @@ func (c *quitCommand) run(ctx context.Context) { return } - err = pb.getPlayerBoard(p.id).setOnline(false).updatePlayerInfo(ctx) + pb.getPlayerBoard(p.id).setOnline(false) + + err = pb.update(ctx, achievementVeryLongOnline) if err != nil { log.Error(err) } @@ -143,7 +147,7 @@ func (c *deathCommand) run(ctx context.Context) { return } - err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) + err = pb.update(ctx, achievementDeathless, achievementBestFeeder) if err != nil { log.Error(err) } @@ -182,7 +186,7 @@ func (c *killCommand) run(ctx context.Context) { return } - err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) + err = pb.update(ctx, achievementMaxFrags, achievementPeaceable) if err != nil { log.Error(err) } @@ -210,9 +214,8 @@ func (c *changeLevelCommand) run(ctx context.Context) { return } - err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) + err = pb.update(ctx, achievementMaxLevel) if err != nil { log.Error(err) } - } diff --git a/playersBoard.go b/playersBoard.go index b948bf0..0a2a31b 100644 --- a/playersBoard.go +++ b/playersBoard.go @@ -3,17 +3,61 @@ package main import ( "context" "fmt" + "strings" "sync" + "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/hako/durafmt" "github.com/pkg/errors" ) +type achievement uint8 + +const ( + _ achievement = iota + achievementBestFeeder + achievementVeryLongOnline + achievementDeathless + achievementPeaceable + achievementMaxFrags + achievementMaxLevel +) + +func getEmojiByAchievement(ach achievement) string { + switch ach { + case achievementBestFeeder: + return "\xF0\x9F\x91\xBB" + case achievementVeryLongOnline: + return "\xF0\x9F\xA4\xAA" + case achievementDeathless: + return "\xF0\x9F\x98\x8E" + case achievementPeaceable: + return "\xF0\x9F\x98\x87" + case achievementMaxFrags: + return "\xF0\x9F\x98\x88" + case achievementMaxLevel: + return "\xF0\x9F\xA7\x99\xE2\x80\x8D\xE2\x99\x82\xEF\xB8\x8F" + default: + return "" + } +} + +var allAchievement = []achievement{ + achievementBestFeeder, + achievementVeryLongOnline, + achievementDeathless, + achievementPeaceable, + achievementMaxFrags, + achievementMaxLevel, +} + type playerInfo struct { playerID string playerMessageID int isOnline bool + achievements []achievement + messageText string } func (p *playerInfo) setOnline(v bool) *playerInfo { @@ -33,23 +77,18 @@ func (p *playerInfo) updatePlayerInfo(ctx context.Context) error { conf := getConfig(ctx) db := getDB(ctx) + lines := make([]string, 0, 10) + player, err := db.getPlayerByID(ctx, p.playerID) if err != nil { return err } - kills, err := db.getKillsByOPlayerID(ctx, player.id) + kills, err := db.getKillsByPlayerID(ctx, player.id) if err != nil { return err } - test := `*%s* | %s -%s Уровень: *%s* -%s Смертей: *%d* -%s Время в игре: %s -%s Фрагов: %d -` - access := "\xE2\x9D\x8C offLine" if p.isOnline { access = "\xE2\x9C\x85 onLine" @@ -67,43 +106,48 @@ func (p *playerInfo) updatePlayerInfo(ctx context.Context) error { panic(err) } - test = fmt.Sprintf(test, player.name, access, - emojiUp, level, - emojiDeaths, player.deaths, - emojiTime, d.Format(units), - emojiGun, kills, - ) + lines = append(lines, fmt.Sprintf("*%s* | %s", player.name, access)) + lines = append(lines, fmt.Sprintf("%s Уровень: *%s*", emojiUp, level)) + lines = append(lines, fmt.Sprintf("%s Смертей: *%d*", emojiDeaths, player.deaths)) + lines = append(lines, fmt.Sprintf("%s Фрагов: *%d*", emojiGun, kills)) + lines = append(lines, fmt.Sprintf("%s Время в игре: *%s*", emojiTime, d.Format(units))) + lines = append(lines, "-----") - if p.playerMessageID != 0 { - mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, test) - mess.ParseMode = tgbotapi.ModeMarkdown + emojiAch := make([]string, 0, len(p.achievements)) - ikm := tgbotapi.NewInlineKeyboardMarkup( - tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData("Подробнее", "additional"), - ), - ) + for _, a := range p.achievements { + emojiAch = append(emojiAch, getEmojiByAchievement(a)) + } - mess.ReplyMarkup = &ikm + lines = append(lines, strings.Join(emojiAch, " ")) - _, err := botApi.Send(mess) - if err != nil { - return errors.Wrap(err, "Failed to update message") + text := strings.Join(lines, "\n") + + if text != p.messageText { + + var c tgbotapi.Chattable + + if p.playerMessageID == 0 { + mess := tgbotapi.NewMessage(conf.ChatID, text) + mess.ParseMode = tgbotapi.ModeMarkdown + + c = mess + } else { + mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, text) + mess.ParseMode = tgbotapi.ModeMarkdown + + c = mess } - return nil + m, err := botApi.Send(c) + if err != nil { + return errors.Wrap(err, "Failed to send message") + } + + p.playerMessageID = m.MessageID + p.messageText = text } - mess := tgbotapi.NewMessage(conf.ChatID, test) - mess.ParseMode = tgbotapi.ModeMarkdown - - m, err := botApi.Send(mess) - if err != nil { - return errors.Wrap(err, "Failed to send message") - } - - p.playerMessageID = m.MessageID - return nil } @@ -120,14 +164,32 @@ func (p *playerInfo) deletePlayerInfo(ctx context.Context) error { return nil } +func (p *playerInfo) setAchievement(ach achievement) { + for _, a := range p.achievements { + if a == ach { + return + } + } + + p.achievements = append(p.achievements, ach) +} + +func (p *playerInfo) unsetAchievement(ach achievement) { + for i, a := range p.achievements { + if a == ach { + p.achievements = append(p.achievements[:i], p.achievements[:i+1]...) + } + } +} + type playersBoard struct { mx sync.Mutex - players []playerInfo + players []*playerInfo } func newPlayersBoard() *playersBoard { return &playersBoard{ - players: make([]playerInfo, 0), + players: make([]*playerInfo, 0), } } @@ -142,19 +204,28 @@ func (pb *playersBoard) load(ctx context.Context) error { return err } - pb.players = make([]playerInfo, 0, len(players)) + pb.players = make([]*playerInfo, 0, len(players)) for _, p := range players { - pi := playerInfo{ + pi := &playerInfo{ playerID: p.id, } - err := pi.updatePlayerInfo(ctx) + pb.players = append(pb.players, pi) + } + + for _, a := range allAchievement { + err := pb.updateAchievements(ctx, a) if err != nil { return err } + } - pb.players = append(pb.players, pi) + for _, p := range pb.players { + err := p.updatePlayerInfo(ctx) + if err != nil { + return err + } } return nil @@ -163,18 +234,110 @@ func (pb *playersBoard) load(ctx context.Context) error { func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo { for i := range pb.players { if pb.players[i].playerID == playerID { - return &pb.players[i] + return pb.players[i] } } pb.mx.Lock() defer pb.mx.Unlock() - pb.players = append(pb.players, playerInfo{ + pb.players = append(pb.players, &playerInfo{ playerID: playerID, }) - return &pb.players[len(pb.players)-1] + return pb.players[len(pb.players)-1] +} + +func (pb *playersBoard) updateAchievements(ctx context.Context, ach achievement) error { + db := getDB(ctx) + + var value int + + playerIDs := make([]string, 0, len(pb.players)) + + setValue := func(v int, playerID string) { + if value < v { + value = v + + playerIDs = []string{playerID} + } else if value == v { + playerIDs = append(playerIDs, playerID) + } + } + + for _, p := range pb.players { + player, err := db.getPlayerByID(ctx, p.playerID) + if err != nil { + return err + } + + switch ach { + case achievementBestFeeder: + setValue(player.deaths, p.playerID) + + case achievementVeryLongOnline: + setValue(int(player.onlineDuration/time.Second), p.playerID) + + case achievementDeathless: + if player.deaths == 0 { + playerIDs = append(playerIDs, p.playerID) + } + + case achievementPeaceable: + kills, err := db.getKillsByPlayerID(ctx, p.playerID) + if err != nil { + return err + } + + setValue(int(^uint(0)>>1)-kills, p.playerID) + + case achievementMaxFrags: + kills, err := db.getKillsByPlayerID(ctx, p.playerID) + if err != nil { + return err + } + + setValue(kills, p.playerID) + + case achievementMaxLevel: + setValue(player.level, p.playerID) + } + } + +mainLoop: + for _, p := range pb.players { + for _, pid := range playerIDs { + if p.playerID == pid { + p.setAchievement(ach) + continue mainLoop + } + } + + p.unsetAchievement(ach) + } + + return nil +} + +func (pb *playersBoard) update(ctx context.Context, as ...achievement) error { + pb.mx.Lock() + defer pb.mx.Unlock() + + for _, a := range as { + err := pb.updateAchievements(ctx, a) + if err != nil { + return err + } + } + + for _, p := range pb.players { + err := p.updatePlayerInfo(ctx) + if err != nil { + return err + } + } + + return nil } func (pb *playersBoard) run(ctx context.Context) {