Moved level state to soko package.

Renamed entity type character and egg to player and target respectively.
This commit is contained in:
Sander Schobers 2019-01-15 21:25:32 +01:00
parent b2342235b3
commit 7634e632fb
13 changed files with 381 additions and 169 deletions

View File

@ -14,9 +14,8 @@ type moveAnimation struct {
pos geom.PointF32 pos geom.PointF32
} }
func newMoveAnimation(e *entity, to geom.Point) *moveAnimation { func newMoveAnimation(e *entity, from, to geom.Point) *moveAnimation {
ani := &moveAnimation{e: e, from: e.pos, to: to, pos: e.pos.ToF32()} ani := &moveAnimation{e: e, from: from, to: to, pos: from.ToF32()}
ani.e.pos = to
return ani return ani
} }

View File

@ -6,8 +6,8 @@ import (
) )
type entity struct { type entity struct {
id int
typ soko.EntityType typ soko.EntityType
pos geom.Point
scr entityLoc scr entityLoc
} }
@ -16,6 +16,6 @@ type entityLoc struct {
z float32 z float32
} }
func newEntity(typ soko.EntityType, pos geom.Point) *entity { func newEntity(e soko.Entity) *entity {
return &entity{typ, pos, entityLoc{pos.ToF32(), 0}} return &entity{e.ID, e.Typ, entityLoc{e.Pos.ToF32(), 0}}
} }

View File

@ -1,35 +0,0 @@
package main
import "opslag.de/schobers/geom"
type entityList []*entity
func (l entityList) Add(e *entity) entityList {
return append(l, e)
}
func (l entityList) AddList(list entityList) entityList {
return append(l, list...)
}
func (l entityList) Find(pos geom.Point) int {
for i, e := range l {
if e.pos == pos {
return i
}
}
return -1
}
func (l entityList) FindEntity(pos geom.Point) *entity {
idx := l.Find(pos)
if idx == -1 {
return nil
}
return l[idx]
}
func (l entityList) Remove(pos geom.Point) entityList {
idx := l.Find(pos)
return append(l[:idx], l[idx+1:]...)
}

View File

@ -0,0 +1,25 @@
package main
import "sort"
type entityMap map[int]*entity
func (m entityMap) RenderOrder() []*entity {
entities := make([]*entity, 0, len(m))
for _, e := range m {
entities = append(entities, e)
}
sort.Slice(entities, func(i, j int) bool {
var posI, posJ = entities[i].scr.pos, entities[j].scr.pos
if posI.Y == posJ.Y {
if posI.X == posJ.X {
return entities[i].id < entities[j].id
}
return posI.X < posJ.X
}
return posI.Y < posJ.Y
})
return entities
}

View File

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"log" "log"
"sort"
"opslag.de/schobers/allg5" "opslag.de/schobers/allg5"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
@ -204,7 +203,7 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
} }
case soko.TileTypeMagma: case soko.TileTypeMagma:
l.drawSprite("magma", "magma", scr) l.drawSprite("magma", "magma", scr)
brick := l.state.FindSunkenBrick(pos) brick := l.state.FindSunkenBrickEntity(pos)
if brick != nil { if brick != nil {
behind, front := splitParticles(scr.pos.Y, l.state.Particles(pos)) behind, front := splitParticles(scr.pos.Y, l.state.Particles(pos))
drawParticles(behind) drawParticles(behind)
@ -215,21 +214,14 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
} }
} }
entities := l.state.Entities() entities := l.state.Entities().RenderOrder()
sort.Slice(entities, func(i, j int) bool {
if entities[i].scr.pos.Y == entities[j].scr.pos.Y {
return entities[i].scr.pos.X < entities[j].scr.pos.X
}
return entities[i].scr.pos.Y < entities[j].scr.pos.Y
})
for _, e := range entities { for _, e := range entities {
switch e.typ { switch e.typ {
case soko.EntityTypeBrick: case soko.EntityTypeBrick:
l.drawSprite("brick", "brick", e.scr) l.drawSprite("brick", "brick", e.scr)
case soko.EntityTypeCharacter: case soko.EntityTypePlayer:
l.drawSprite("dragon", "dragon", e.scr) l.drawSprite("dragon", "dragon", e.scr)
case soko.EntityTypeEgg: case soko.EntityTypeTarget:
l.drawSprite("egg", "egg", e.scr) l.drawSprite("egg", "egg", e.scr)
} }
} }

View File

@ -15,10 +15,11 @@ type playLevelState struct {
pack levelPack pack levelPack
level soko.Level level soko.Level
state soko.State
player *entity player *entity
egg *entity egg *entity
bricks entityList bricks entityMap
sunken entityList sunken entityMap
splash map[geom.Point]*splashAnimation splash map[geom.Point]*splashAnimation
steps int steps int
@ -30,9 +31,14 @@ type playLevelState struct {
keysDown keyPressedState keysDown keyPressedState
} }
func (s *playLevelState) Entities() entityList { func (s *playLevelState) Entities() entityMap {
var entities entityList entities := entityMap{}
return entities.Add(s.player).Add(s.egg).AddList(s.bricks) entities[s.player.id] = s.player
entities[s.egg.id] = s.egg
for id, e := range s.bricks {
entities[id] = e
}
return entities
} }
func (s *playLevelState) Particles(at geom.Point) []particle { func (s *playLevelState) Particles(at geom.Point) []particle {
@ -49,34 +55,34 @@ func (s *playLevelState) Particles(at geom.Point) []particle {
return particles return particles
} }
func (s *playLevelState) FindSunkenBrick(pos geom.Point) *entity { func (s *playLevelState) FindSunkenBrickEntity(pos geom.Point) *entity {
return s.sunken.FindEntity(pos) id, ok := s.state.SunkenBricks[pos]
if ok {
return s.sunken[id]
}
return nil
} }
func (s *playLevelState) IsNextToMagma(pos geom.Point) bool { func (s *playLevelState) IsNextToMagma(pos geom.Point) bool {
return s.checkTile(pos.Add2D(1, 0), s.isMagma) || return s.state.Any(soko.IsMagma, soko.Neighbours(pos)...)
s.checkTile(pos.Add2D(-1, 0), s.isMagma) ||
s.checkTile(pos.Add2D(0, -1), s.isMagma) ||
s.checkTile(pos.Add2D(0, 1), s.isMagma)
} }
func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func()) { func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func()) {
s.ctx = ctx s.ctx = ctx
s.pack = ctx.Levels.ByID(pack) s.pack = ctx.Levels.ByID(pack)
s.level = s.pack.levels[level] s.level = s.pack.levels[level]
s.state = s.level.State()
s.bricks = nil s.bricks = nil
s.sunken = nil s.sunken = nil
s.splash = map[geom.Point]*splashAnimation{} s.splash = map[geom.Point]*splashAnimation{}
for i, e := range s.level.Entities {
switch e { s.player = newEntity(s.state.Player)
case soko.EntityTypeBrick: s.egg = newEntity(s.state.Target)
s.bricks = append(s.bricks, newEntity(e, s.level.IdxToPos(i))) s.bricks = entityMap{}
case soko.EntityTypeCharacter: for pos, id := range s.state.Bricks {
s.player = newEntity(e, s.level.IdxToPos(i)) s.bricks[id] = newEntity(soko.Entity{ID: id, Pos: pos, Typ: soko.EntityTypeBrick})
case soko.EntityTypeEgg:
s.egg = newEntity(e, s.level.IdxToPos(i))
}
} }
s.sunken = entityMap{}
s.keysDown = keyPressedState{} s.keysDown = keyPressedState{}
s.onComplete = onComplete s.onComplete = onComplete
} }
@ -98,42 +104,45 @@ func (s *playLevelState) Tick(now time.Duration) {
} }
func (s *playLevelState) TryPlayerMove(key allg5.Key) { func (s *playLevelState) TryPlayerMove(key allg5.Key) {
var dir geom.Point var dir soko.Direction
switch key { switch key {
case s.ctx.Settings.Controls.MoveUp: case s.ctx.Settings.Controls.MoveUp:
dir = geom.Pt(0, -1) dir = soko.DirectionUp
case s.ctx.Settings.Controls.MoveRight: case s.ctx.Settings.Controls.MoveRight:
dir = geom.Pt(1, 0) dir = soko.DirectionRight
case s.ctx.Settings.Controls.MoveDown: case s.ctx.Settings.Controls.MoveDown:
dir = geom.Pt(0, 1) dir = soko.DirectionDown
case s.ctx.Settings.Controls.MoveLeft: case s.ctx.Settings.Controls.MoveLeft:
dir = geom.Pt(-1, 0) dir = soko.DirectionLeft
default: default:
return return
} }
s.tryPlayerMove(dir, key) s.tryPlayerMove(dir)
} }
func (s *playLevelState) tryPlayerMove(dir geom.Point, key allg5.Key) { func (s *playLevelState) tryPlayerMove(dir soko.Direction) {
if s.player.scr.pos != s.player.pos.ToF32() { if s.player.scr.pos != s.state.Player.Pos.ToF32() {
return return
} }
to := s.player.pos.Add(dir) state, ok := s.state.MovePlayer(dir)
if !s.canMove(s.player.pos, dir) { if !ok {
log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key)) log.Printf("Move is not allowed (tried out move %s)", dir.String())
return return
} }
if brick := s.bricks.FindEntity(to); brick != nil { to := state.Player.Pos
log.Printf("Pushing brick at %s", to)
brickTo := to.Add(dir) if brickID, ok := s.state.Bricks[to]; ok {
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, brickTo), func() { log.Printf("Brick %d moved", brickID)
brick := s.bricks[brickID]
brickTo := state.Entities[brickID].Pos
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, to, brickTo), func() {
log.Printf("Brick movement finished") log.Printf("Brick movement finished")
if s.checkTile(brickTo, s.wouldBrickSink) { if sunkenBrickID, ok := state.SunkenBricks[brickTo]; ok && sunkenBrickID == brickID {
log.Printf("Sinking brick at %s", brickTo) log.Printf("Sinking brick %d", brickID)
s.bricks = s.bricks.Remove(brickTo) delete(s.bricks, brickID)
s.sunken = s.sunken.Add(brick) s.sunken[brickID] = brick
s.ani.Start(s.ctx.Tick, newSinkAnimation(brick)) s.ani.Start(s.ctx.Tick, newSinkAnimation(brick))
splash := newSplashAnimation(brickTo) splash := newSplashAnimation(brickTo)
@ -147,9 +156,9 @@ func (s *playLevelState) tryPlayerMove(dir geom.Point, key allg5.Key) {
s.steps++ s.steps++
log.Printf("Moving player to %s", to) log.Printf("Moving player to %s", to)
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, to), func() { s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, s.state.Player.Pos, to), func() {
log.Printf("Player movement finished") log.Printf("Player movement finished")
if s.player.pos == s.egg.pos { if to == state.Target.Pos {
s.complete = true s.complete = true
if onComplete := s.onComplete; onComplete != nil { if onComplete := s.onComplete; onComplete != nil {
onComplete() onComplete()
@ -157,76 +166,11 @@ func (s *playLevelState) tryPlayerMove(dir geom.Point, key allg5.Key) {
} else { } else {
pressed := s.keysDown.ArePressed(s.ctx.Settings.Controls.MovementKeys()...) pressed := s.keysDown.ArePressed(s.ctx.Settings.Controls.MovementKeys()...)
if len(pressed) == 1 { if len(pressed) == 1 {
key := pressed[0]
log.Printf("Movement key %s is down, moving further", gut.KeyToString(key)) log.Printf("Movement key %s is down, moving further", gut.KeyToString(key))
s.TryPlayerMove(pressed[0]) s.TryPlayerMove(key)
} }
} }
}) })
} s.state = state
func (s *playLevelState) canMove(from, dir geom.Point) bool {
to := from.Add(dir)
if !s.checkTile(to, s.isSolidTile) {
return false
}
brick := s.bricks.FindEntity(to)
if brick != nil {
brickTo := to.Add(dir)
return !s.checkTileNotFound(brickTo, s.isObstructed, true)
}
return true
}
func (s *playLevelState) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t soko.TileType) bool) bool {
return s.checkTileNotFound(pos, check, false)
}
func (s *playLevelState) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t soko.TileType) bool, notFound bool) bool {
idx := s.level.PosToIdx(pos)
if idx == -1 {
return notFound
}
return check(pos, idx, s.level.Tiles[idx])
}
func (s *playLevelState) findEntityAt(pos geom.Point) *entity {
if s.player.pos == pos {
return s.player
}
brick := s.bricks.FindEntity(pos)
if brick != nil {
return brick
}
return s.sunken.FindEntity(pos)
}
func (s *playLevelState) isObstructed(pos geom.Point, idx int, t soko.TileType) bool {
if s.bricks.FindEntity(pos) != nil {
return true // brick
}
switch s.level.Tiles[idx] {
case soko.TileTypeMagma:
return false
case soko.TileTypeBasic:
return false
}
return true
}
func (s *playLevelState) isMagma(pos geom.Point, idx int, t soko.TileType) bool {
return t == soko.TileTypeMagma
}
func (s *playLevelState) isSolidTile(pos geom.Point, idx int, t soko.TileType) bool {
switch t {
case soko.TileTypeBasic:
return true
case soko.TileTypeMagma:
return s.sunken.FindEntity(pos) != nil
}
return false
}
func (s *playLevelState) wouldBrickSink(pos geom.Point, idx int, t soko.TileType) bool {
return t == soko.TileTypeMagma && s.sunken.FindEntity(pos) == nil
} }

50
soko/direction.go Normal file
View File

@ -0,0 +1,50 @@
package soko
import "opslag.de/schobers/geom"
type Direction int
const (
DirectionUp Direction = iota
DirectionRight
DirectionDown
DirectionLeft
)
var Directions = [4]Direction{DirectionUp, DirectionRight, DirectionDown, DirectionLeft}
func (d Direction) String() string {
switch d {
case DirectionUp:
return "up"
case DirectionRight:
return "right"
case DirectionDown:
return "down"
case DirectionLeft:
return "left"
}
return "invalid"
}
func (d Direction) ToPoint() geom.Point {
switch d {
case DirectionUp:
return geom.Pt(0, -1)
case DirectionRight:
return geom.Pt(1, 0)
case DirectionDown:
return geom.Pt(0, 1)
case DirectionLeft:
return geom.Pt(-1, 0)
}
return geom.Point{}
}
func Neighbours(p geom.Point) []geom.Point {
neighbours := make([]geom.Point, 4)
for i, dir := range Directions {
neighbours[i] = p.Add(dir.ToPoint())
}
return neighbours
}

19
soko/entity.go Normal file
View File

@ -0,0 +1,19 @@
package soko
import "opslag.de/schobers/geom"
type Entity struct {
ID int
Pos geom.Point
Typ EntityType
}
type Entities map[int]Entity
func (e Entities) Clone() Entities {
clone := Entities{}
for id, e := range e {
clone[id] = e
}
return clone
}

31
soko/entitylocations.go Normal file
View File

@ -0,0 +1,31 @@
package soko
import "opslag.de/schobers/geom"
type EntityLocations map[geom.Point]int
func (l EntityLocations) Add(p geom.Point, id int) { l[p] = id }
func (l EntityLocations) Clone() EntityLocations {
clone := EntityLocations{}
for p, id := range l {
clone[p] = id
}
return clone
}
func (l EntityLocations) Has(p geom.Point) bool {
_, ok := l[p]
return ok
}
func (l EntityLocations) Move(from, to geom.Point) {
id, ok := l[from]
if !ok {
panic("no entitiy at position")
}
l.Remove(from)
l.Add(to, id)
}
func (l EntityLocations) Remove(p geom.Point) { delete(l, p) }

View File

@ -3,18 +3,18 @@ package soko
type EntityType byte type EntityType byte
const ( const (
EntityTypeInvalid EntityType = EntityType(0) EntityTypeInvalid EntityType = EntityType(0)
EntityTypeNone = '_' EntityTypeNone = '_'
EntityTypeCharacter = '@' EntityTypePlayer = '@'
EntityTypeEgg = 'X' EntityTypeTarget = 'X'
EntityTypeBrick = 'B' EntityTypeBrick = 'B'
) )
func (e EntityType) IsValid() bool { func (e EntityType) IsValid() bool {
switch e { switch e {
case EntityTypeNone: case EntityTypeNone:
case EntityTypeCharacter: case EntityTypePlayer:
case EntityTypeEgg: case EntityTypeTarget:
case EntityTypeBrick: case EntityTypeBrick:
default: default:
return false return false

View File

@ -19,3 +19,29 @@ func (l Level) PosToIdx(p geom.Point) int {
} }
return p.Y*l.Width + p.X return p.Y*l.Width + p.X
} }
func (l Level) State() State {
s := NewState()
for i, t := range l.Tiles {
pos := l.IdxToPos(i)
switch t {
case TileTypeBasic:
s.BasicTiles.Add(pos)
case TileTypeMagma:
s.MagmaTiles.Add(pos)
}
}
for i, e := range l.Entities {
pos := l.IdxToPos(i)
switch e {
case EntityTypeBrick:
s.AddBrick(pos)
case EntityTypePlayer:
s.SetPlayer(pos)
case EntityTypeTarget:
s.SetTarget(pos)
}
}
return s
}

17
soko/locations.go Normal file
View File

@ -0,0 +1,17 @@
package soko
import "opslag.de/schobers/geom"
type Locations map[geom.Point]bool
func (l Locations) Add(p geom.Point) { l[p] = true }
func (l Locations) Clone() Locations {
clone := Locations{}
for p := range l {
clone[p] = true
}
return clone
}
func (l Locations) Remove(p geom.Point) { delete(l, p) }

144
soko/state.go Normal file
View File

@ -0,0 +1,144 @@
package soko
import "opslag.de/schobers/geom"
type State struct {
Player Entity
Target Entity
BasicTiles Locations
MagmaTiles Locations
Bricks EntityLocations
SunkenBricks EntityLocations
Entities Entities
nextEntityID int
}
func NewState() State {
return State{
BasicTiles: Locations{},
MagmaTiles: Locations{},
Bricks: EntityLocations{},
SunkenBricks: EntityLocations{},
Entities: Entities{},
}
}
// Clone creates and returns a clone of the state.
func (s State) Clone() State {
return State{
Player: s.Player,
Target: s.Target,
BasicTiles: s.BasicTiles.Clone(),
MagmaTiles: s.MagmaTiles.Clone(),
Bricks: s.Bricks.Clone(),
SunkenBricks: s.SunkenBricks.Clone(),
Entities: s.Entities.Clone(),
nextEntityID: s.nextEntityID,
}
}
func (s *State) addEntity(pos geom.Point, typ EntityType, add func(Entity)) {
id := s.nextEntityID
e := Entity{id, pos, typ}
add(e)
s.Entities[id] = e
s.nextEntityID++
}
func (s *State) AddBrick(pos geom.Point) {
s.addEntity(pos, EntityTypeBrick, func(e Entity) {
s.Bricks.Add(pos, e.ID)
})
}
func (s State) All(pred func(State, geom.Point) bool, p ...geom.Point) bool {
for _, p := range p {
if !pred(s, p) {
return false
}
}
return true
}
func (s State) Any(pred func(State, geom.Point) bool, p ...geom.Point) bool {
for _, p := range p {
if pred(s, p) {
return true
}
}
return false
}
func (s State) IsOpenForBrick(p geom.Point) bool {
if s.IsWalkable(p) {
return !s.Bricks.Has(p)
}
return s.MagmaTiles[p]
}
// IsWalkable indicates that a player could walk over this tile regardless of any bricks that might be there.
func (s State) IsWalkable(p geom.Point) bool {
return s.BasicTiles[p] || (s.MagmaTiles[p] && s.SunkenBricks.Has(p))
}
// MovePlayer tries to move the player in the specified direction. Returns the new state and true if the move is valid. Returns the current state and false is the move was invalid.
func (s State) MovePlayer(dir Direction) (State, bool) {
to := s.Player.Pos.Add(dir.ToPoint())
if !s.IsWalkable(to) {
return s, false
}
if !s.Bricks.Has(to) {
return s.Mutate(func(s *State) {
s.movePlayer(to)
}), true
}
brickTo := to.Add(dir.ToPoint())
if !s.IsOpenForBrick(brickTo) {
return s, false
}
brickID := s.Bricks[to]
return s.Mutate(func(s *State) {
s.movePlayer(to)
if s.MagmaTiles[brickTo] && !s.SunkenBricks.Has(brickTo) {
s.Bricks.Remove(to)
s.SunkenBricks.Add(brickTo, brickID)
} else {
s.Bricks.Move(to, brickTo)
}
s.Entities[brickID] = Entity{brickID, brickTo, EntityTypeBrick}
}), true
}
// Mutate clones the state, applies the mutation on the clone and returns the clone.
func (s State) Mutate(fn func(s *State)) State {
clone := s.Clone()
fn(&clone)
return clone
}
func (s *State) movePlayer(to geom.Point) {
s.Player.Pos = to
s.Entities[s.Player.ID] = Entity{s.Player.ID, to, EntityTypePlayer}
}
func (s *State) SetPlayer(pos geom.Point) {
s.addEntity(pos, EntityTypePlayer, func(e Entity) {
s.Player = e
})
}
func (s *State) SetTarget(pos geom.Point) {
s.addEntity(pos, EntityTypeTarget, func(e Entity) {
s.Target = e
})
}
func IsMagma(s State, p geom.Point) bool { return s.MagmaTiles[p] }