diff --git a/cmd/krampus19/animations.go b/cmd/krampus19/animations.go index ab1c4f9..c253241 100644 --- a/cmd/krampus19/animations.go +++ b/cmd/krampus19/animations.go @@ -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 } diff --git a/cmd/krampus19/entity.go b/cmd/krampus19/entity.go index b2fc03a..8652d5e 100644 --- a/cmd/krampus19/entity.go +++ b/cmd/krampus19/entity.go @@ -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}} } diff --git a/cmd/krampus19/entitylist.go b/cmd/krampus19/entitylist.go deleted file mode 100644 index 16b6c53..0000000 --- a/cmd/krampus19/entitylist.go +++ /dev/null @@ -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:]...) -} diff --git a/cmd/krampus19/entitymap.go b/cmd/krampus19/entitymap.go new file mode 100644 index 0000000..96d369c --- /dev/null +++ b/cmd/krampus19/entitymap.go @@ -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 +} diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index 6bb5552..7b21a21 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -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) } } diff --git a/cmd/krampus19/playlevelstate.go b/cmd/krampus19/playlevelstate.go index b391abb..0c52f82 100644 --- a/cmd/krampus19/playlevelstate.go +++ b/cmd/krampus19/playlevelstate.go @@ -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 } diff --git a/soko/direction.go b/soko/direction.go new file mode 100644 index 0000000..288ae89 --- /dev/null +++ b/soko/direction.go @@ -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 +} diff --git a/soko/entity.go b/soko/entity.go new file mode 100644 index 0000000..5b46142 --- /dev/null +++ b/soko/entity.go @@ -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 +} diff --git a/soko/entitylocations.go b/soko/entitylocations.go new file mode 100644 index 0000000..7ae1ca5 --- /dev/null +++ b/soko/entitylocations.go @@ -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) } diff --git a/soko/entitytype.go b/soko/entitytype.go index 6be1485..6b98bcd 100644 --- a/soko/entitytype.go +++ b/soko/entitytype.go @@ -3,18 +3,18 @@ package soko type EntityType byte const ( - EntityTypeInvalid EntityType = EntityType(0) - EntityTypeNone = '_' - EntityTypeCharacter = '@' - EntityTypeEgg = 'X' - EntityTypeBrick = 'B' + EntityTypeInvalid EntityType = EntityType(0) + EntityTypeNone = '_' + 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 diff --git a/soko/level.go b/soko/level.go index 46ec4bf..03d902f 100644 --- a/soko/level.go +++ b/soko/level.go @@ -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 +} diff --git a/soko/locations.go b/soko/locations.go new file mode 100644 index 0000000..171ad45 --- /dev/null +++ b/soko/locations.go @@ -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) } diff --git a/soko/state.go b/soko/state.go new file mode 100644 index 0000000..a9015a1 --- /dev/null +++ b/soko/state.go @@ -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] }