1
0
Fork 0
This commit is contained in:
Arthur K. 2026-05-14 20:19:22 +03:00
commit 7cc71fb719
Signed by: wzray
GPG key ID: B97F30FDC4636357
13 changed files with 840 additions and 0 deletions

View 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...)))
}