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
}
func newMoveAnimation(e *entity, to geom.Point) *moveAnimation {
ani := &moveAnimation{e: e, from: e.pos, to: to, pos: e.pos.ToF32()}
ani.e.pos = to
func newMoveAnimation(e *entity, from, to geom.Point) *moveAnimation {
ani := &moveAnimation{e: e, from: from, to: to, pos: from.ToF32()}
return ani
}

View File

@ -6,8 +6,8 @@ import (
)
type entity struct {
id int
typ soko.EntityType
pos geom.Point
scr entityLoc
}
@ -16,6 +16,6 @@ type entityLoc struct {
z float32
}
func newEntity(typ soko.EntityType, pos geom.Point) *entity {
return &entity{typ, pos, entityLoc{pos.ToF32(), 0}}
func newEntity(e soko.Entity) *entity {
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 (
"fmt"
"log"
"sort"
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
@ -204,7 +203,7 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
}
case soko.TileTypeMagma:
l.drawSprite("magma", "magma", scr)
brick := l.state.FindSunkenBrick(pos)
brick := l.state.FindSunkenBrickEntity(pos)
if brick != nil {
behind, front := splitParticles(scr.pos.Y, l.state.Particles(pos))
drawParticles(behind)
@ -215,21 +214,14 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
}
}
entities := l.state.Entities()
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
})
entities := l.state.Entities().RenderOrder()
for _, e := range entities {
switch e.typ {
case soko.EntityTypeBrick:
l.drawSprite("brick", "brick", e.scr)
case soko.EntityTypeCharacter:
case soko.EntityTypePlayer:
l.drawSprite("dragon", "dragon", e.scr)
case soko.EntityTypeEgg:
case soko.EntityTypeTarget:
l.drawSprite("egg", "egg", e.scr)
}
}

View File

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

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

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

View File

@ -19,3 +19,29 @@ func (l Level) PosToIdx(p geom.Point) int {
}
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] }