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