190 lines
3.8 KiB
Go
190 lines
3.8 KiB
Go
package challenge
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Engine struct {
|
|
cfg Config
|
|
openAt time.Duration
|
|
closeAt time.Duration
|
|
regularFloors int
|
|
players map[int]*Player
|
|
order map[int]bool
|
|
logs []string
|
|
}
|
|
|
|
func NewEngine(cfg Config) (*Engine, error) {
|
|
openAt, err := parseClock(cfg.OpenAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.Floors < 1 {
|
|
return nil, errors.New("Floors must be positive")
|
|
}
|
|
if cfg.Monsters < 0 {
|
|
return nil, errors.New("Monsters must not be negative")
|
|
}
|
|
if cfg.Duration <= 0 {
|
|
return nil, errors.New("Duration must be positive")
|
|
}
|
|
return &Engine{
|
|
cfg: cfg,
|
|
openAt: openAt,
|
|
closeAt: openAt + time.Duration(cfg.Duration)*time.Hour,
|
|
regularFloors: max(0, cfg.Floors-1),
|
|
players: map[int]*Player{},
|
|
order: map[int]bool{},
|
|
}, nil
|
|
}
|
|
|
|
func (e *Engine) Apply(event Event) {
|
|
e.closeExpired(event.At)
|
|
|
|
p := e.player(event.Player)
|
|
if p.Terminal {
|
|
return
|
|
}
|
|
if !p.Registered && event.ID != 1 {
|
|
p.Disqual = true
|
|
p.Terminal = true
|
|
p.EndedAt = event.At
|
|
e.log(event.At, "Player [%d] is disqualified", p.ID)
|
|
return
|
|
}
|
|
|
|
switch event.ID {
|
|
case 1:
|
|
e.register(event, p)
|
|
case 2:
|
|
e.enterDungeon(event, p)
|
|
case 3:
|
|
e.killMonster(event, p)
|
|
case 4:
|
|
e.nextFloor(event, p)
|
|
case 5:
|
|
e.previousFloor(event, p)
|
|
case 6:
|
|
e.enterBoss(event, p)
|
|
case 7:
|
|
e.killBoss(event, p)
|
|
case 8:
|
|
e.leaveDungeon(event, p)
|
|
case 9:
|
|
e.cannotContinue(event, p)
|
|
case 10:
|
|
e.restoreHealth(event, p)
|
|
case 11:
|
|
e.takeDamage(event, p)
|
|
default:
|
|
e.impossible(event)
|
|
}
|
|
}
|
|
|
|
func (e *Engine) Output() string {
|
|
e.closeExpired(e.closeAt)
|
|
|
|
var b strings.Builder
|
|
for _, line := range e.logs {
|
|
b.WriteString(line)
|
|
b.WriteByte('\n')
|
|
}
|
|
b.WriteString("Final report:\n")
|
|
|
|
ids := make([]int, 0, len(e.order))
|
|
for id := range e.order {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Ints(ids)
|
|
for _, id := range ids {
|
|
p := e.players[id]
|
|
state := e.state(p)
|
|
spent := time.Duration(0)
|
|
if p.Attempted {
|
|
spent = p.EndedAt - p.EnteredAt
|
|
if spent < 0 {
|
|
spent = 0
|
|
}
|
|
}
|
|
b.WriteString(fmt.Sprintf("[%s] %d [%s, %s, %s] HP:%d\n", state, p.ID, formatDuration(spent), formatDuration(e.avgFloorTime(p)), formatDuration(p.BossTime), p.Health))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (e *Engine) player(id int) *Player {
|
|
if p, ok := e.players[id]; ok {
|
|
return p
|
|
}
|
|
p := &Player{
|
|
ID: id,
|
|
Health: 100,
|
|
MonsterKills: make([]int, e.cfg.Floors+1),
|
|
FloorDurations: make([]time.Duration, e.cfg.Floors+1),
|
|
}
|
|
e.players[id] = p
|
|
e.order[id] = true
|
|
return p
|
|
}
|
|
|
|
func (e *Engine) closeExpired(now time.Duration) {
|
|
if now < e.closeAt {
|
|
return
|
|
}
|
|
for _, p := range e.players {
|
|
if p.InDungeon && !p.Terminal {
|
|
e.pauseFloorTimer(p, e.closeAt)
|
|
p.InDungeon = false
|
|
p.Terminal = true
|
|
p.EndedAt = e.closeAt
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Engine) floorCleared(p *Player, floor int) bool {
|
|
if floor > e.regularFloors {
|
|
return true
|
|
}
|
|
return p.MonsterKills[floor] >= e.cfg.Monsters
|
|
}
|
|
|
|
func (e *Engine) allRegularFloorsCleared(p *Player) bool {
|
|
for floor := 1; floor <= e.regularFloors; floor++ {
|
|
if !e.floorCleared(p, floor) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (e *Engine) avgFloorTime(p *Player) time.Duration {
|
|
var total time.Duration
|
|
cleared := 0
|
|
for floor := 1; floor <= e.regularFloors; floor++ {
|
|
if e.floorCleared(p, floor) {
|
|
total += p.FloorDurations[floor]
|
|
cleared++
|
|
}
|
|
}
|
|
if cleared == 0 {
|
|
return 0
|
|
}
|
|
return total / time.Duration(cleared)
|
|
}
|
|
|
|
func (e *Engine) state(p *Player) ResultState {
|
|
if p.Disqual {
|
|
return StateDisqual
|
|
}
|
|
if e.allRegularFloorsCleared(p) && p.BossKilled {
|
|
return StateSuccess
|
|
}
|
|
return StateFail
|
|
}
|
|
|
|
func (e *Engine) log(at time.Duration, format string, args ...any) {
|
|
e.logs = append(e.logs, fmt.Sprintf("[%s] %s", formatClock(at), fmt.Sprintf(format, args...)))
|
|
}
|