init
This commit is contained in:
commit
7cc71fb719
13 changed files with 840 additions and 0 deletions
190
internal/challenge/engine.go
Normal file
190
internal/challenge/engine.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
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...)))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue