package tins2021 import ( "math/rand" "sort" "opslag.de/schobers/geom" ) type Level struct { Player geom.Point Lives int Stars int StarsCollected int Tiles Tiles Monsters Monsters MonsterTargets map[geom.Point]geom.Point GameOver bool Difficulty int Score int Bounds geom.Rectangle } func NewLevel() *Level { const dims = 12 f := &Level{ Player: geom.Pt(1, 1), Lives: 3, Stars: 0, StarsCollected: 0, Tiles: Tiles{}, Monsters: Monsters{}, MonsterTargets: map[geom.Point]geom.Point{}, Bounds: geom.Rect(1, 1, dims+1, dims+1), } for y := 1; y <= dims; y++ { endX := dims if y%2 == 0 { endX-- } for x := 1; x <= endX; x++ { f.Tiles[geom.Pt(x, y)] = &Tile{} } } return f } func (l Level) CanPlayerMove(dir Direction) (geom.Point, bool) { return l.Tiles.CanMove(l.Player, dir) } func (l Level) CanMonsterMove(p geom.Point, dir Direction) (geom.Point, bool) { q, ok := l.Tiles.CanMove(p, dir) if !ok { return geom.Point{}, false } if l.CanMonsterMoveTo(q) { return q, true } return geom.Point{}, false } func (l Level) CanMonsterMoveTo(p geom.Point) bool { if l.Tiles[p].Occupied() { return false } if _, ok := l.Monsters[p]; ok { return false } for _, target := range l.MonsterTargets { if p == target { return false } } return true } func (l *Level) DecrementLive() { l.Lives-- if l.Lives == 0 { l.GameOver = true } } func (l *Level) DestroyMonster(pos geom.Point) { delete(l.MonsterTargets, pos) delete(l.Monsters, pos) } func (l *Level) MoveMonster(target, source geom.Point) { l.Monsters[target] = l.Monsters[source] l.DestroyMonster(source) } type MonsterHit struct { Position geom.Point Type MonsterType } func (l *Level) MovePlayer(dir Direction) (bool, *MonsterHit) { towards, allowed := l.CanPlayerMove(dir) if !allowed { return false, nil } l.Player = towards tile := l.Tiles[towards] if tile.Heart { l.Lives++ tile.Heart = false l.Score += 10 } if tile.Star { l.StarsCollected++ tile.Star = false l.Score += 25 } var hit *MonsterHit if l.Monsters[towards] != nil { hit = &MonsterHit{ Position: towards, Type: l.Monsters[towards].Type(), } l.DecrementLive() l.DestroyMonster(towards) l.Score -= 5 } l.Score -= 1 // for every move return true, hit } func (l *Level) Randomize(difficulty int, stars int) { if difficulty < 0 { difficulty = 0 } l.Difficulty = difficulty positions := make([]geom.Point, 0, len(l.Tiles)) for pos := range l.Tiles { positions = append(positions, pos) } distances := l.Tiles.Distances(l.Player) sort.Slice(positions, func(i, j int) bool { return distances[positions[i]] < distances[positions[j]] }) pick := func(bias int) geom.Point { // bias towards far away [0..100] if bias > 100 { bias = 100 } if bias < 0 { bias = 0 } n := len(positions) const min = 4 from := min + bias*(n-min)/(2*(n-min)) // never pick first return positions[from+rand.Intn(n-from)] } flip := difficulty * len(l.Tiles) / 200 if flip > len(l.Tiles)/2 { flip = len(l.Tiles) / 2 } for ; flip > 0; flip-- { for { pos := pick(0) if l.Tiles[pos].Inversed { continue } l.Tiles[pos].Invert() if l.Tiles.AllReachable(l.Player) { break } l.Tiles[pos].Invert() } } l.Stars = stars for stars > 0 { pos := pick(difficulty) if l.Tiles[pos].Occupied() { continue } l.Tiles[pos].Star = true stars-- } hearts := rand.Intn(2 + (100-difficulty)/50) // [3..1] (only difficulty 0 has 3 hearts) for hearts > 0 { pos := pick(difficulty) if l.Tiles[pos].Occupied() { continue } l.Tiles[pos].Heart = true hearts-- } monsters := 5 + (25 * difficulty / 100) minRandomMonster := (100-difficulty)*60/100 + 10 // [70..10] minChaserMonster := (100-difficulty)*70/100 + 25 // [95..25] for monsters > 0 { pos := pick(100 - difficulty) curr := l.Monsters[pos] if l.Tiles[pos].Occupied() || curr != nil { continue } monster := MonsterTypeStraight m := rand.Intn(100) if m >= minChaserMonster { monster = MonsterTypeChaser } else if m >= minRandomMonster { monster = MonsterTypeRandom } switch monster { case MonsterTypeStraight: l.Monsters[pos] = &StraightWalkingMonster{Direction: RandomDirection()} case MonsterTypeRandom: l.Monsters[pos] = &RandomWalkingMonster{} case MonsterTypeChaser: l.Monsters[pos] = &ChasingMonster{} default: panic("not implemented") } monsters-- } }