core/internal/domain/player.go

488 lines
9.7 KiB
Go

package domain
import (
"cmp"
"fmt"
"math"
"net/url"
"slices"
"time"
)
const (
playerNameMinLength = 1
playerNameMaxLength = 150
)
type Player struct {
id int
serverKey string
name string
numVillages int
points int
rank int
tribeID int
od OpponentsDefeated
profileURL *url.URL
bestRank int
bestRankAt time.Time
mostPoints int
mostPointsAt time.Time
mostVillages int
mostVillagesAt time.Time
lastActivityAt time.Time
createdAt time.Time
deletedAt time.Time
}
// UnmarshalPlayerFromDatabase unmarshals Player from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalPlayerFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalPlayerFromDatabase(
id int,
serverKey string,
name string,
numVillages int,
points int,
rank int,
tribeID int,
od OpponentsDefeated,
rawProfileURL string,
bestRank int,
bestRankAt time.Time,
mostPoints int,
mostPointsAt time.Time,
mostVillages int,
mostVillagesAt time.Time,
lastActivityAt time.Time,
createdAt time.Time,
deletedAt time.Time,
) (Player, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return Player{}, ValidationError{
Model: playerModelName,
Field: "id",
Err: err,
}
}
profileURL, err := parseURL(rawProfileURL)
if err != nil {
return Player{}, ValidationError{
Model: playerModelName,
Field: "profileURL",
Err: err,
}
}
return Player{
id: id,
serverKey: serverKey,
name: name,
numVillages: numVillages,
points: points,
rank: rank,
tribeID: tribeID,
od: od,
profileURL: profileURL,
bestRank: bestRank,
bestRankAt: bestRankAt,
mostPoints: mostPoints,
mostPointsAt: mostPointsAt,
mostVillages: mostVillages,
mostVillagesAt: mostVillagesAt,
lastActivityAt: lastActivityAt,
createdAt: createdAt,
deletedAt: deletedAt,
}, nil
}
const playerModelName = "Player"
func (p Player) ID() int {
return p.id
}
func (p Player) ServerKey() string {
return p.serverKey
}
func (p Player) Name() string {
return p.name
}
func (p Player) NumVillages() int {
return p.numVillages
}
func (p Player) Points() int {
return p.points
}
func (p Player) Rank() int {
return p.rank
}
func (p Player) TribeID() int {
return p.tribeID
}
func (p Player) OD() OpponentsDefeated {
return p.od
}
func (p Player) ProfileURL() *url.URL {
return p.profileURL
}
func (p Player) BestRank() int {
return p.bestRank
}
func (p Player) BestRankAt() time.Time {
return p.bestRankAt
}
func (p Player) MostPoints() int {
return p.mostPoints
}
func (p Player) MostPointsAt() time.Time {
return p.mostPointsAt
}
func (p Player) MostVillages() int {
return p.mostVillages
}
func (p Player) MostVillagesAt() time.Time {
return p.mostVillagesAt
}
func (p Player) LastActivityAt() time.Time {
return p.lastActivityAt
}
func (p Player) CreatedAt() time.Time {
return p.createdAt
}
func (p Player) DeletedAt() time.Time {
return p.deletedAt
}
func (p Player) Base() BasePlayer {
return BasePlayer{
id: p.id,
name: p.name,
numVillages: p.numVillages,
points: p.points,
rank: p.rank,
tribeID: p.tribeID,
od: p.od,
profileURL: p.profileURL,
}
}
func (p Player) IsDeleted() bool {
return !p.deletedAt.IsZero()
}
type Players []Player
// Delete finds all tribes that are not in the given slice with active tribes and returns their ids.
// Both slices must be sorted in ascending order by ID.
func (ps Players) Delete(active BasePlayers) []int {
//nolint:prealloc
var toDelete []int
for _, p := range ps {
_, found := slices.BinarySearchFunc(active, p, func(a BasePlayer, b Player) int {
return cmp.Compare(a.ID(), b.ID())
})
if found {
continue
}
toDelete = append(toDelete, p.ID())
}
return toDelete
}
type CreatePlayerParams struct {
base BasePlayer
serverKey string
bestRank int
bestRankAt time.Time
mostPoints int
mostPointsAt time.Time
mostVillages int
mostVillagesAt time.Time
lastActivityAt time.Time
}
const createPlayerParamsModelName = "CreatePlayerParams"
// NewCreatePlayerParams constructs a slice of CreatePlayerParams based on the given parameters.
// Both slices must be sorted in ascending order by ID
// + if storedPlayers contains tribes from different servers. they must be sorted in ascending order by server key.
//
//nolint:gocyclo
func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers Players) ([]CreatePlayerParams, error) {
if err := validateServerKey(serverKey); err != nil {
return nil, ValidationError{
Model: createPlayerParamsModelName,
Field: "serverKey",
Err: err,
}
}
params := make([]CreatePlayerParams, 0, len(players))
for i, player := range players {
if player.IsZero() {
return nil, fmt.Errorf("players[%d] is an empty struct", i)
}
var old Player
idx, found := slices.BinarySearchFunc(storedPlayers, player, func(a Player, b BasePlayer) int {
if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 {
return res
}
return cmp.Compare(a.ID(), b.ID())
})
if found {
old = storedPlayers[idx]
}
now := time.Now()
p := CreatePlayerParams{
base: player,
serverKey: serverKey,
bestRank: player.Rank(),
bestRankAt: now,
mostPoints: player.Points(),
mostPointsAt: now,
mostVillages: player.NumVillages(),
mostVillagesAt: now,
lastActivityAt: now,
}
if old.ID() > 0 {
if old.MostPoints() >= p.MostPoints() {
p.mostPoints = old.MostPoints()
p.mostPointsAt = old.MostPointsAt()
}
if old.MostVillages() >= p.MostVillages() {
p.mostVillages = old.MostVillages()
p.mostVillagesAt = old.MostVillagesAt()
}
if old.BestRank() <= p.BestRank() {
p.bestRank = old.BestRank()
p.bestRankAt = old.BestRankAt()
}
//nolint:lll
if player.Points() <= old.Points() && player.NumVillages() <= old.NumVillages() && player.OD().ScoreAtt() <= old.OD().ScoreAtt() {
p.lastActivityAt = old.LastActivityAt()
}
}
params = append(params, p)
}
return params, nil
}
func (c CreatePlayerParams) Base() BasePlayer {
return c.base
}
func (c CreatePlayerParams) ServerKey() string {
return c.serverKey
}
func (c CreatePlayerParams) BestRank() int {
return c.bestRank
}
func (c CreatePlayerParams) BestRankAt() time.Time {
return c.bestRankAt
}
func (c CreatePlayerParams) MostPoints() int {
return c.mostPoints
}
func (c CreatePlayerParams) MostPointsAt() time.Time {
return c.mostPointsAt
}
func (c CreatePlayerParams) MostVillages() int {
return c.mostVillages
}
func (c CreatePlayerParams) MostVillagesAt() time.Time {
return c.mostVillagesAt
}
func (c CreatePlayerParams) LastActivityAt() time.Time {
return c.lastActivityAt
}
type PlayerSort uint8
const (
PlayerSortIDASC PlayerSort = iota + 1
PlayerSortIDDESC
PlayerSortServerKeyASC
PlayerSortServerKeyDESC
)
const PlayerListMaxLimit = 200
type ListPlayersParams struct {
ids []int
idGT NullInt
serverKeys []string
deleted NullBool
sort []PlayerSort
limit int
offset int
}
const listPlayersParamsModelName = "ListPlayersParams"
func NewListPlayersParams() ListPlayersParams {
return ListPlayersParams{
sort: []PlayerSort{
PlayerSortServerKeyASC,
PlayerSortIDASC,
},
limit: PlayerListMaxLimit,
}
}
func (params *ListPlayersParams) IDs() []int {
return params.ids
}
func (params *ListPlayersParams) SetIDs(ids []int) error {
for i, id := range ids {
if err := validateIntInRange(id, 0, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listPlayersParamsModelName,
Field: "ids",
Index: i,
Err: err,
}
}
}
params.ids = ids
return nil
}
func (params *ListPlayersParams) IDGT() NullInt {
return params.idGT
}
func (params *ListPlayersParams) SetIDGT(idGT NullInt) error {
if idGT.Valid {
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "idGT",
Err: err,
}
}
}
params.idGT = idGT
return nil
}
func (params *ListPlayersParams) ServerKeys() []string {
return params.serverKeys
}
func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
params.serverKeys = serverKeys
return nil
}
func (params *ListPlayersParams) Deleted() NullBool {
return params.deleted
}
func (params *ListPlayersParams) SetDeleted(deleted NullBool) error {
params.deleted = deleted
return nil
}
func (params *ListPlayersParams) Sort() []PlayerSort {
return params.sort
}
const (
playerSortMinLength = 1
playerSortMaxLength = 2
)
func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
if err := validateSliceLen(sort, playerSortMinLength, playerSortMaxLength); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListPlayersParams) Limit() int {
return params.limit
}
func (params *ListPlayersParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, PlayerListMaxLimit); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "limit",
Err: err,
}
}
params.limit = limit
return nil
}
func (params *ListPlayersParams) Offset() int {
return params.offset
}
func (params *ListPlayersParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "offset",
Err: err,
}
}
params.offset = offset
return nil
}