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
}
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) {

2
go.mod
View File

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

View File

@ -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)
}
}

View File

@ -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) {