221 lines
4.5 KiB
Go
221 lines
4.5 KiB
Go
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--
|
|
}
|
|
}
|