add achievements
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Timofey.Kovalev 2021-06-22 02:34:22 +03:00
parent 3302634f61
commit 4d5b25a761
4 changed files with 249 additions and 96 deletions

View File

@ -68,7 +68,11 @@ type Player struct {
level int 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 ( var (
err error err error
lastOnline string 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) { 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" query := "SELECT id, name, last_login, online_duration, deaths, level FROM players WHERE id = $1"
rows, err := db.db.QueryContext(ctx, query, id) row := db.db.QueryRowContext(ctx, query, id)
if err != nil { if err := row.Err(); err != nil {
return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) return nil, errors.Wrapf(err, "Failed to run db query [%s]", query)
} }
defer rows.Close()
if rows.Next() {
var p Player var p Player
err = fetchPlayer(rows, &p) err := fetchPlayer(row, &p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &p, nil return &p, nil
}
return nil, nil
} }
func (db *dbLayer) getPlayerByName(ctx context.Context, name string) (*Player, error) { 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" 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) row := db.db.QueryRowContext(ctx, query, name)
if err != nil { if err := row.Err(); err != nil {
return nil, errors.Wrapf(err, "Failed to run db query [%s]", query) return nil, errors.Wrapf(err, "Failed to run db query [%s]", query)
} }
defer rows.Close()
if rows.Next() {
var p Player var p Player
err = fetchPlayer(rows, &p) err := fetchPlayer(row, &p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &p, nil return &p, nil
}
return nil, 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" query := "SELECT SUM(count) FROM killings WHERE player_id = $1 GROUP BY player_id"
rows, err := db.db.QueryContext(ctx, query, playerID) row := db.db.QueryRowContext(ctx, query, playerID)
if err != nil { if err := row.Err(); err != nil {
return 0, errors.Wrapf(err, "Failed to run db query [%s]", query) return 0, errors.Wrapf(err, "Failed to run db query [%s]", query)
} }
defer rows.Close()
if rows.Next() {
var killsNum int var killsNum int
err = rows.Scan(&killsNum) err := row.Scan(&killsNum)
if err != nil { if err != nil {
return 0, errors.Wrap(err, "Failed to fetch data base") return 0, errors.Wrap(err, "Failed to fetch data base")
} }
return killsNum, nil return killsNum, nil
}
return 0, nil
} }
func (db *dbLayer) createPlayer(ctx context.Context, name string, lastOnline time.Time) (*Player, error) { func (db *dbLayer) createPlayer(ctx context.Context, name string, lastOnline time.Time) (*Player, error) {

2
go.mod
View File

@ -1,4 +1,4 @@
module mk-statistics module craft-bot
go 1.16 go 1.16

View File

@ -49,7 +49,7 @@ func (c *joinCommand) run(ctx context.Context) {
log := getLogger(ctx) log := getLogger(ctx)
pb := getDPlayersBoard(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) m, err := botApi.Send(mess)
if err != nil { if err != nil {
log.Error(err) 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 { if err != nil {
log.Error(err) log.Error(err)
} }
@ -95,7 +97,7 @@ func (c *quitCommand) run(ctx context.Context) {
log := getLogger(ctx) log := getLogger(ctx)
pb := getDPlayersBoard(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) m, err := botApi.Send(mess)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@ -115,7 +117,9 @@ func (c *quitCommand) run(ctx context.Context) {
return return
} }
err = pb.getPlayerBoard(p.id).setOnline(false).updatePlayerInfo(ctx) pb.getPlayerBoard(p.id).setOnline(false)
err = pb.update(ctx, achievementVeryLongOnline)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -143,7 +147,7 @@ func (c *deathCommand) run(ctx context.Context) {
return return
} }
err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) err = pb.update(ctx, achievementDeathless, achievementBestFeeder)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -182,7 +186,7 @@ func (c *killCommand) run(ctx context.Context) {
return return
} }
err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) err = pb.update(ctx, achievementMaxFrags, achievementPeaceable)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -210,9 +214,8 @@ func (c *changeLevelCommand) run(ctx context.Context) {
return return
} }
err = pb.getPlayerBoard(p.id).updatePlayerInfo(ctx) err = pb.update(ctx, achievementMaxLevel)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
} }

View File

@ -3,17 +3,61 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"sync" "sync"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/hako/durafmt" "github.com/hako/durafmt"
"github.com/pkg/errors" "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 { type playerInfo struct {
playerID string playerID string
playerMessageID int playerMessageID int
isOnline bool isOnline bool
achievements []achievement
messageText string
} }
func (p *playerInfo) setOnline(v bool) *playerInfo { func (p *playerInfo) setOnline(v bool) *playerInfo {
@ -33,23 +77,18 @@ func (p *playerInfo) updatePlayerInfo(ctx context.Context) error {
conf := getConfig(ctx) conf := getConfig(ctx)
db := getDB(ctx) db := getDB(ctx)
lines := make([]string, 0, 10)
player, err := db.getPlayerByID(ctx, p.playerID) player, err := db.getPlayerByID(ctx, p.playerID)
if err != nil { if err != nil {
return err return err
} }
kills, err := db.getKillsByOPlayerID(ctx, player.id) kills, err := db.getKillsByPlayerID(ctx, player.id)
if err != nil { if err != nil {
return err return err
} }
test := `*%s* | %s
%s Уровень: *%s*
%s Смертей: *%d*
%s Время в игре: %s
%s Фрагов: %d
`
access := "\xE2\x9D\x8C offLine" access := "\xE2\x9D\x8C offLine"
if p.isOnline { if p.isOnline {
access = "\xE2\x9C\x85 onLine" access = "\xE2\x9C\x85 onLine"
@ -67,42 +106,47 @@ func (p *playerInfo) updatePlayerInfo(ctx context.Context) error {
panic(err) panic(err)
} }
test = fmt.Sprintf(test, player.name, access, lines = append(lines, fmt.Sprintf("*%s* | %s", player.name, access))
emojiUp, level, lines = append(lines, fmt.Sprintf("%s Уровень: *%s*", emojiUp, level))
emojiDeaths, player.deaths, lines = append(lines, fmt.Sprintf("%s Смертей: *%d*", emojiDeaths, player.deaths))
emojiTime, d.Format(units), lines = append(lines, fmt.Sprintf("%s Фрагов: *%d*", emojiGun, kills))
emojiGun, kills, lines = append(lines, fmt.Sprintf("%s Время в игре: *%s*", emojiTime, d.Format(units)))
) lines = append(lines, "-----")
if p.playerMessageID != 0 { emojiAch := make([]string, 0, len(p.achievements))
mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, test)
mess.ParseMode = tgbotapi.ModeMarkdown
ikm := tgbotapi.NewInlineKeyboardMarkup( for _, a := range p.achievements {
tgbotapi.NewInlineKeyboardRow( emojiAch = append(emojiAch, getEmojiByAchievement(a))
tgbotapi.NewInlineKeyboardButtonData("Подробнее", "additional"),
),
)
mess.ReplyMarkup = &ikm
_, err := botApi.Send(mess)
if err != nil {
return errors.Wrap(err, "Failed to update message")
} }
return nil lines = append(lines, strings.Join(emojiAch, " "))
}
mess := tgbotapi.NewMessage(conf.ChatID, test) 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 mess.ParseMode = tgbotapi.ModeMarkdown
m, err := botApi.Send(mess) c = mess
} else {
mess := tgbotapi.NewEditMessageText(conf.ChatID, p.playerMessageID, text)
mess.ParseMode = tgbotapi.ModeMarkdown
c = mess
}
m, err := botApi.Send(c)
if err != nil { if err != nil {
return errors.Wrap(err, "Failed to send message") return errors.Wrap(err, "Failed to send message")
} }
p.playerMessageID = m.MessageID p.playerMessageID = m.MessageID
p.messageText = text
}
return nil return nil
} }
@ -120,14 +164,32 @@ func (p *playerInfo) deletePlayerInfo(ctx context.Context) error {
return nil 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 { type playersBoard struct {
mx sync.Mutex mx sync.Mutex
players []playerInfo players []*playerInfo
} }
func newPlayersBoard() *playersBoard { func newPlayersBoard() *playersBoard {
return &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 return err
} }
pb.players = make([]playerInfo, 0, len(players)) pb.players = make([]*playerInfo, 0, len(players))
for _, p := range players { for _, p := range players {
pi := playerInfo{ pi := &playerInfo{
playerID: p.id, 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 { if err != nil {
return err return err
} }
}
pb.players = append(pb.players, pi) for _, p := range pb.players {
err := p.updatePlayerInfo(ctx)
if err != nil {
return err
}
} }
return nil return nil
@ -163,18 +234,110 @@ func (pb *playersBoard) load(ctx context.Context) error {
func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo { func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo {
for i := range pb.players { for i := range pb.players {
if pb.players[i].playerID == playerID { if pb.players[i].playerID == playerID {
return &pb.players[i] return pb.players[i]
} }
} }
pb.mx.Lock() pb.mx.Lock()
defer pb.mx.Unlock() defer pb.mx.Unlock()
pb.players = append(pb.players, playerInfo{ pb.players = append(pb.players, &playerInfo{
playerID: playerID, 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) { func (pb *playersBoard) run(ctx context.Context) {