;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; playsim module.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(begin-module 'game)
(module-export '(
  step-time-report
  ;;
  max-field-width
  max-field-height
  ;;
  field-index->coords
  field-index->x
  field-index->y
  field-width
  field-height
  field
  ;;
  field-otype-ref
  field-edef-ref
  field-eobj-ref
  field-image-index-ref
  field-prop-at?
  field-get-prop-at
  ;;
  tick
  ;; for sounds
  seen-active-magic-walls?
  seen-active-amoeba?
  ;;
  option-ref
  option-props-ref
  has-override-option?
  ;;
  new-cave
  flatten-bdcff
  ;;
  spawn-at
  spawn-at-forced
  move-obj
  spawn-move-at
  ;;
  explode-at
  explode-at-voodoo-special
  ;;
  step
  ;; history, for chasing stone
  player-chase-x
  player-chase-y
  ;;
  KEY_LEFT
  KEY_RIGHT
  KEY_UP
  KEY_DOWN
  KEY_SNAP
  KEY_DROP
  ;;
  key-down
  key-up
  key-pressed?
  key-reset
  ;;
  diamond-price
  diamond-collected
  amoeba-processed
  amoeba-2-processed
  magic-wall-scanned
  add-score
  activate-magic-walls
  magic-walls-active?
  ;;
  player-found
  player-hatch
  init-slime-pprng-seed
  ;;
  DIAMOND_KEY_IDX
  has-key?
  key-collected
  key-used
  ;;
  random
  random-range-incl
  ;;
  pprng?
  ?pprng
  make-pprng-2seeds
  make-pprng-u8
  pprng-next
  ;;
  pprng-u8
  ;;
  start-recording
  replay-put-word
  replay-put-play-sound
  wipe-replay
  report-replay-size
  get-recorded-data
  ;;
  set-replay-data
  in-replay?
  replay-finished?
  ;;
  reset-cheats
  cheat-open-gate
  ;;
  save-state
  load-state
  wipe-state
))
(module-export-r/w '(
  map-cave-hash-u32  ;; calculated by flattener
  map-cave-hash-str  ;; RIPEMD-160 hash context, calculated by flattener
  full-cave-hash-u32  ;; calculated by flattener
  full-cave-hash-str  ;; RIPEMD-160 hash context, calculated by flattener
  cave-index
  cave-level
  cave-name
  cave-step-time
  cave-time
  cave-total-time
  hatch-time
  diamonds-required
  diamond-value
  extra-diamond-value
  viewport-x
  viewport-y
  viewport-w
  viewport-h
  ;;
  ;game-replay ;; see below (bytevector)
  ;game-replay-pos
  ;replay-data ;; if we are replaying right now
  ;replay-data-pos
  ;;
  has-bad-objects
  ;;
  amoeba-count
  amoeba-can-spread
  ;; not implemented yet
  amoeba-2-count
  amoeba-2-can-spread
  ;; the following need to be set from BDCFF
  amoeba-growth-prob
  amoeba-fast-growth-prob
  amoeba-threshold
  amoeba-2-growth-prob
  amoeba-2-fast-growth-prob
  amoeba-2-threshold
  amoeba-time
  amoeba-timer-active
  magic-timer-active
  ;; state:
  ;;   'SLEEPING
  ;;   'AWAKE
  ;;   'TOO-BIG
  ;;   'ENCLOSED
  amoeba-state
  amoeba-2-state
  ;;
  cave-finished
  diamond-count
  ;;
  kill-player
  player-suicide-request
  voodoo-touched
  ;;
  magic-wall-state
  magic-wall-initial-timer
  magic-wall-timer
  sweet-eaten
  bombs-collected
  bomb-is-k8
  got-pneumatic-hammer
  creatures-backwards
  ;;
  last-dir-left
  last-move-hdir
  ;;
  cave-score
  gate-open
  gate-flash
  was-explosion
  ;; biters
  biter-delay-frame
  biters-wait-frame
  ;; replicators
  replicator-delay-frame
  replicators-wait-frame
  ;;
  player-dead ;; set by the scanner
  player-must-explode
  player-count
  player-inbox    ;; set to #t if active player is inbox (there could be other non-inbox players, though)
  player-hatched  ;; set to #t if the player is hatched from the inbox (but other active players might be already present)
  player-x
  player-y
  ;;
  cheat-harmless-mobiles
  ;;
  slime-pprng-seed-inited
  ;;
  game-pprng  ;; random seed
  set-pprng-seed-u8
  set-pprng-seed-u16
))
(import '(
  ::etype
  ::edef
  ::bdcff-loader
))


(constant max-field-width (if use-256x256-map 256 128))
(constant max-field-height (if use-256x256-map 256 128))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; cave options.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define cave-options-override #nil)

;; get game option (default or overriden).
;; for level-dependent options, get the option for the current level.
;; "level-dependent" means that the option is a vector.
define option-ref(name)
  define res
    ::option-ref-over default-cave-options cave-options-override name
  ;; hack!
  if {{not {name eq? 'slime-eat-convert}} and {vector? res}}
    {res[cave-level]}
    res

;; get game option (default or overriden).
;; do not "unwrap" level-dependent options.
;; "level-dependent" means that the option is a vector.
define option-ref-nounwrap(name)
  ::option-ref-over default-cave-options cave-options-override name

;; get all option properties as assoc list.
define option-props-ref(name)
  ::option-props-ref-over default-cave-options cave-options-override name

;; check if the given option is overriden.
define has-override-option?(name)
  has-option? cave-options-override name


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; global state
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; `field` is a vector.
;; it holds entity index (etype).
(define field #nil)
;; `field-time` is another vector.
;; it holds game tick when the entity was last active.
;; that is, the entity should perform its action only
;; when `field-time[idx] < game:tick`.
;; spawn procedures set field time to the current tick
;; (i.e. newly spawned objects are inactive on the current game frame).
(define field-time #nil)
;; field size
(define field-width 0)
(define field-height 0)

;; game tick
(define tick 0)
;; used for player animation.
(define last-dir-left #t)
(define last-move-hdir #nil)  ;; #nil: none; #t: horiz; #f: vert

;; set by error reporter.
(define bad-objects-reported #nil)

;; modifiable
(define map-cave-hash-u32 0)
(define map-cave-hash-str "")
(define full-cave-hash-u32 0)
(define full-cave-hash-str "")
(define cave-index 0)
(define cave-level 0)
(define cave-name "Cave")
(define cave-step-time 200)
(define cave-time 130)
(define cave-total-time 0)
(define hatch-time 21)
(define diamonds-required 4)
(define diamond-value 99)
(define extra-diamond-value 4)

;; 6502 PRNG state.
(define game-pprng cons(0 #x1E))

(define viewport-x 0)
(define viewport-y 0)
(define viewport-w 40)
(define viewport-h 22)

;; byte vector with frame changes.
;; encoding: idx new-value
;; special commands (idx):
;;  -1: end of frame; next 2 values: C64 PPRNG state at the end of the frame
;;  -2: end of cave (finished)
;;  -3: end of cave (died)
;;  -4: end of cave (aborted)
;;  -5: end of cave (skipped)
;;  -8: player coords (x y) -- used for camera panning
;; -16: magic wall active on this frame
;; -17: amoeba active on this frame
;; -32: diamond collected
;; -33: score change (arg: delta)
;; -42: exit opened
;; -43: was explosion
;; -44: slime pprng was inited (no args)
;; -46: play sound (arg: sndid)
;;      sndid:
;;       1: GD_S_EXPLOSION
;;       2: GD_S_GHOST_EXPLOSION
;;       3: GD_S_BOMB_EXPLOSION
;;       4: GD_S_VOODOO_EXPLOSION
;; -60: play object sound (arg: otype)
;; -61: play object walk sound (arg: otype)
;; -62: play object push sound (arg: otype)
;; -63: play object fall sound (arg: otype)
;; -69: player hatched
;;  -666: initial frame(always first)
;;     args: width, height, field
;; first frame contains the whole initial map
(define game-replay #nil)
(define game-replay-pos 0)
(define replay-data #nil)
(define replay-data-pos 0)
(define replay-otype-trans #nil)

define get-recorded-data()
  if {bytevector? game-replay}
    bytevector-slice game-replay 0 game-replay-pos
    #nil

;; called before loading a new cave (by "new-cave").
define reset-options()
  gset! viewport-x 0
  gset! viewport-y 0
  gset! viewport-w 40
  gset! viewport-h 22
  gset! map-cave-hash-u32 0
  gset! map-cave-hash-str ""
  gset! full-cave-hash-u32 0
  gset! full-cave-hash-str ""
  gset! cave-index 0
  gset! cave-level 0
  gset! cave-name "Cave"
  gset! cave-step-time 200
  gset! cave-time 130
  gset! cave-total-time 0
  gset! hatch-time 21
  gset! diamonds-required 4
  gset! diamond-value 99
  gset! extra-diamond-value 4
  if {bytevector? game-replay}
    bytevector-resize! game-replay 0
  gset! game-replay #nil  ;; do not save replay
  gset! game-replay-pos 0
  if {bytevector? replay-data}
    bytevector-resize! replay-data 0
  gset! replay-data #nil  ;; do not save replay
  gset! replay-otype-trans #nil
  if {vector? replay-otype-trans}
    vector-resize! replay-otype-trans 0
  gset! replay-data-pos 0


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; gameplay flags
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; various flags, and other gameplay state.
;; reset on cave (re)start.
(define cave-finished #f)
(define kill-player #f)
(define player-suicide-request #f)
(define voodoo-touched #f)
(define magic-wall-state #f)  ;; #f: dormant; #t: active; #nil: expired
(define magic-wall-timer 0)
(define magic-wall-initial-timer 1)
(define sweet-eaten #f)
(define bombs-collected 0)
(define bomb-is-k8 #t)
(define got-pneumatic-hammer #f)
(define creatures-backwards #f)
(define key-count #nil)
(define diamond-count 0)
(define cave-score 0)
(define gate-open #f)
(define gate-flash #f)
(define was-explosion #f)
(define player-dead #f)
(define player-count 0)
(define player-inbox #t)
(define player-hatched #f)
(define player-missing-frames 0)
(define player-must-explode #f)
(define player-x 0)
(define player-y 0)
;; "player history" is a simply circular buffer.
;; it is used for chasing stones.
(define player-history-x #nil) ;; actually, vectors
(define player-history-y #nil)
(define player-history-stpos 0) ;; position to store or read
(define magic-walls-count 0)
(define amoeba-count 0)
(define amoeba-can-spread #f)
(define amoeba-2-count 0)
(define amoeba-2-can-spread #f)
(define amoeba-state 'SLEEPING)
(define amoeba-2-state 'SLEEPING)
(define amoeba-growth-prob 31_250)
(define amoeba-fast-growth-prob 250_000)
(define amoeba-threshold 200)
(define amoeba-2-growth-prob 31_250)
(define amoeba-2-fast-growth-prob 250_000)
(define amoeba-2-threshold 200)
(define amoeba-time {999 * 5})
(define amoeba-timer-active #f)
(define magic-timer-active #t)
(define has-bad-objects #f)
(define slime-pprng-seed-inited #f)
(define creature-reverse-timer-active #f)
(define creature-reverse-timer-init 0)
(define creature-reverse-timer 0)
(define biter-delay-frame 0)
(define biters-wait-frame 0)
(define replicator-delay-frame 4)
(define replicators-wait-frame 0)


;; called before loading a new cave (by "new-cave").
define reset-flags()
  gset! cave-finished #f
  gset! kill-player #f
  gset! player-suicide-request #f
  gset! voodoo-touched #f
  gset! magic-wall-state #f
  gset! magic-wall-timer 0
  gset! magic-wall-initial-timer 1
  gset! sweet-eaten #f
  gset! bombs-collected 0
  gset! bomb-is-k8 #f
  gset! got-pneumatic-hammer #f
  gset! creatures-backwards #f
  gset! key-count #nil
  gset! diamond-count 0
  gset! cave-score 0
  gset! gate-open #f
  gset! gate-flash #f
  gset! was-explosion #f
  gset! player-dead #f
  gset! player-count 0
  gset! player-inbox #t
  gset! player-hatched #f
  gset! player-missing-frames 0
  gset! player-must-explode #f
  gset! player-x -1
  gset! player-y -1
  gset! player-history-x #nil ;; actually, vectors
  gset! player-history-y #nil
  gset! player-history-stpos 0
  gset! magic-walls-count 0
  gset! amoeba-count 0
  gset! amoeba-2-count 0
  gset! amoeba-can-spread #f
  gset! amoeba-2-can-spread #f
  gset! amoeba-state 'SLEEPING
  gset! amoeba-2-state 'SLEEPING
  gset! amoeba-growth-prob 31_250
  gset! amoeba-fast-growth-prob 250_000
  gset! amoeba-threshold 200
  gset! amoeba-2-growth-prob 31_250
  gset! amoeba-2-fast-growth-prob 250_000
  gset! amoeba-2-threshold 200
  gset! amoeba-time {999 * 5}
  gset! amoeba-timer-active #f
  gset! magic-timer-active #f
  gset! has-bad-objects #f
  gset! slime-pprng-seed-inited #f
  gset! creature-reverse-timer-active #f
  gset! creature-reverse-timer-init 0
  gset! creature-reverse-timer 0
  gset! biter-delay-frame 0
  gset! biters-wait-frame 0
  gset! replicator-delay-frame 4
  gset! replicators-wait-frame 0


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; game state saving and loading.
;; not intended to be saved on disk.
;; WARNING! "load-state" should be called on the same cave!
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; free non-heap memory. it will be freed by GC too, but this is better.
define wipe-state(state)
  if {bytevector? state}
    bytevector-resize! state 0

define save-state()
  define state make-bytevector()
  ;;
  define put-byte(n)
    assert {{fixnum? n} and between?(n 0 255)}
    bytevector-push! state n
  ;;
  define put-boolean(n)
    assert {boolean? n}
    put-byte (if n 1 0)
  ;;
  define put-fixnum(n)
    assert {fixnum? n}
    cond
      {negative? n}
        {n := abs(n)}
        assert {n >= 0}
        cond
          {n <= 255}
            put-byte 2
            put-byte bit-and(n #xFF)
          {n <= 65535}
            put-byte 3
            put-byte bit-and(n #xFF)
            put-byte bit-and(bit-shr(n 8) #xFF)
          {n <= #xFF_FF_FF}
            put-byte 4
            put-byte bit-and(n #xFF)
            put-byte bit-and(bit-shr(n 8) #xFF)
            put-byte bit-and(bit-shr(n 16) #xFF)
          else
            assert {n <= #x3F_FF_FF_FF} string-append("negative state fixnum too big:" number->string(n))
            put-byte 5
            put-byte bit-and(n #xFF)
            put-byte bit-and(bit-shr(n 8) #xFF)
            put-byte bit-and(bit-shr(n 16) #xFF)
            put-byte bit-and(bit-shr(n 24) #xFF)
      else
        cond
          {n <= 255}
            put-byte 6
            put-byte bit-and(n #xFF)
          {n <= 65535}
            put-byte 7
            put-byte bit-and(n #xFF)
            put-byte bit-and(bit-shr(n 8) #xFF)
          {n <= #xFF_FF_FF}
            put-byte 8
            put-byte bit-and(n #xFF)
            put-byte bit-and(bit-shr(n 8) #xFF)
            put-byte bit-and(bit-shr(n 16) #xFF)
          else
            assert {n <= #x3F_FF_FF_FF} string-append("state fixnum too big:" number->string(n))
            put-byte 9
            put-byte bit-and(n #xFF)
            put-byte bit-and(bit-shr(n 8) #xFF)
            put-byte bit-and(bit-shr(n 16) #xFF)
            put-byte bit-and(bit-shr(n 24) #xFF)
  ;;
  define put-value(v)
    cond
      {boolean? v}  put-boolean(v)
      {fixnum? v}   put-fixnum(v)
      {null? v}     put-byte(127)
      else error(string-append("invalid state value: " SYSTEM:type-name))
  ;;
  define put-player-history-vector(vec)
    if {vector? vec}
      then
        put-byte 126
        put-byte vector-length(vec)
        define idx 0
        while {idx <> vector-length(vec)}
          put-value vec[idx]
          inc! idx
      else
        assert {null? vec}
        put-byte 127
  ;;
  define put-amoeba-state(st)
    case st
      (SLEEPING)  put-byte(110)
      (AWAKE)     put-byte(111)
      (TOO-BIG)   put-byte(112)
      (ENCLOSED)  put-byte(113)
      else error("invalid amoeba state")
  ;;
  define put-bjprng-state()
    define pst random-state-ref()
    put-byte bytevector-length(pst)
    define idx 0
    while {idx <> bytevector-length(pst)}
      put-byte pst[idx]
      inc! idx
  ;;
  put-bjprng-state()
  ;;
  put-value(game-replay-pos)
  ;;
  put-value(player-history-stpos)
  put-player-history-vector(player-history-x)
  put-player-history-vector(player-history-y)
  ;;
  put-amoeba-state(amoeba-state)
  put-amoeba-state(amoeba-2-state)
  ;;
  put-value(car(game-pprng))
  put-value(cdr(game-pprng))
  ;;
  put-value(tick)
  put-value(last-dir-left)
  put-value(last-move-hdir)
  ;;
  put-value(cave-time)
  put-value(cave-finished)
  put-value(kill-player)
  put-value(player-suicide-request)
  put-value(voodoo-touched)
  put-value(magic-wall-state)
  put-value(magic-wall-timer)
  put-value(magic-wall-initial-timer)
  put-value(sweet-eaten)
  put-value(bombs-collected)
  put-value(bomb-is-k8)
  put-value(got-pneumatic-hammer)
  put-value(creatures-backwards)
  put-value(key-count)
  put-value(diamond-count)
  put-value(cave-score)
  put-value(gate-open)
  put-value(gate-flash)
  put-value(was-explosion)
  put-value(player-dead)
  put-value(player-count)
  put-value(player-inbox)
  put-value(player-hatched)
  put-value(player-missing-frames)
  put-value(player-must-explode)
  put-value(player-x)
  put-value(player-y)
  put-value(magic-walls-count)
  put-value(amoeba-count)
  put-value(amoeba-can-spread)
  put-value(amoeba-2-count)
  put-value(amoeba-2-can-spread)
  put-value(amoeba-growth-prob)
  put-value(amoeba-fast-growth-prob)
  put-value(amoeba-threshold)
  put-value(amoeba-2-growth-prob)
  put-value(amoeba-2-fast-growth-prob)
  put-value(amoeba-2-threshold)
  put-value(amoeba-time)
  put-value(amoeba-timer-active)
  put-value(magic-timer-active)
  put-value(has-bad-objects)
  put-value(slime-pprng-seed-inited)
  put-value(creature-reverse-timer-active)
  put-value(creature-reverse-timer-init)
  put-value(creature-reverse-timer)
  put-value(biter-delay-frame)
  put-value(biters-wait-frame)
  put-value(replicator-delay-frame)
  put-value(replicators-wait-frame)
  ;;
  ; save field
  define x
  define y 0
  while {y <> field-height}
    {x := 0}
    while {x <> field-width}
      put-value(field[field-index-at(x y)])
      put-value({field-time[field-index-at(x y)] - tick})
      inc! x
    inc! y
  ;;
  state


define load-state(state)
  assert {bytevector? state}
  define stpos 0
  ;;
  define get-byte()
    define res state[stpos]
    inc! stpos
    assert {{fixnum? res} and between?(res 0 255)}
    res
  ;;
  define get-value()
    define type get-byte()
    define b0
    define b1
    define b2
    define b3
    cond
      {type = 0}
        #f
      {type = 1}
        #t
      {type = 2}
        {- get-byte()}
      {type = 3}
        {b0 := get-byte()}
        {b1 := get-byte()}
        {- {b0 + bit-shl(b1 8)}}
      {type = 4}
        {b0 := get-byte()}
        {b1 := get-byte()}
        {b2 := get-byte()}
        {- {b0 + bit-shl(b1 8) + bit-shl(b2 16)}}
      {type = 5}
        {b0 := get-byte()}
        {b1 := get-byte()}
        {b2 := get-byte()}
        {b3 := get-byte()}
        {- {b0 + bit-shl(b1 8) + bit-shl(b2 16) + bit-shl(b3 24)}}
      {type = 6}
        get-byte()
      {type = 7}
        {b0 := get-byte()}
        {b1 := get-byte()}
        {b0 + bit-shl(b1 8)}
      {type = 8}
        {b0 := get-byte()}
        {b1 := get-byte()}
        {b2 := get-byte()}
        {b0 + bit-shl(b1 8) + bit-shl(b2 16)}
      {type = 9}
        {b0 := get-byte()}
        {b1 := get-byte()}
        {b2 := get-byte()}
        {b3 := get-byte()}
        {b0 + bit-shl(b1 8) + bit-shl(b2 16) + bit-shl(b3 24)}
      {type = 110}
        'SLEEPING
      {type = 111}
        'AWAKE
      {type = 112}
        'TOO-BIG
      {type = 113}
        'ENCLOSED
      {type = 126}
        {b2 := make-vector(get-byte())}
        {b1 := 0}
        while {b1 <> vector-length(b2)}
          {b2[b1] := get-value()}
          inc! b1
        b2
      {type = 127}
        #nil
      else error(string-append("invalid state value: " number->string(type)))
  ;;
  define get-bjprng-state()
    define sz get-byte()
    define pst make-bytevector(sz)
    define idx 0
    while {idx <> bytevector-length(pst)}
      {pst[idx] := get-byte()}
      inc! idx
    random-state-set! pst
  ;;
  get-bjprng-state()
  ;;
  gset! game-replay-pos get-value()
  ;;
  gset! player-history-stpos get-value()
  gset! player-history-x get-value()
  gset! player-history-y get-value()
  ;;
  gset! amoeba-state get-value()
  gset! amoeba-2-state get-value()
  ;;
  car-set! game-pprng get-value()
  cdr-set! game-pprng get-value()
  ;;
  gset! tick get-value()
  gset! last-dir-left get-value()
  gset! last-move-hdir get-value()
  ;;
  gset! cave-time get-value()
  gset! cave-finished get-value()
  gset! kill-player get-value()
  gset! player-suicide-request get-value()
  gset! voodoo-touched get-value()
  gset! magic-wall-state get-value()
  gset! magic-wall-timer get-value()
  gset! magic-wall-initial-timer get-value()
  gset! sweet-eaten get-value()
  gset! bombs-collected get-value()
  gset! bomb-is-k8 get-value()
  gset! got-pneumatic-hammer get-value()
  gset! creatures-backwards get-value()
  gset! key-count get-value()
  gset! diamond-count get-value()
  gset! cave-score get-value()
  gset! gate-open get-value()
  gset! gate-flash get-value()
  gset! was-explosion get-value()
  gset! player-dead get-value()
  gset! player-count get-value()
  gset! player-inbox get-value()
  gset! player-hatched get-value()
  gset! player-missing-frames get-value()
  gset! player-must-explode get-value()
  gset! player-x get-value()
  gset! player-y get-value()
  gset! magic-walls-count get-value()
  gset! amoeba-count get-value()
  gset! amoeba-can-spread get-value()
  gset! amoeba-2-count get-value()
  gset! amoeba-2-can-spread get-value()
  gset! amoeba-growth-prob get-value()
  gset! amoeba-fast-growth-prob get-value()
  gset! amoeba-threshold get-value()
  gset! amoeba-2-growth-prob get-value()
  gset! amoeba-2-fast-growth-prob get-value()
  gset! amoeba-2-threshold get-value()
  gset! amoeba-time get-value()
  gset! amoeba-timer-active get-value()
  gset! magic-timer-active get-value()
  gset! has-bad-objects get-value()
  gset! slime-pprng-seed-inited get-value()
  gset! creature-reverse-timer-active get-value()
  gset! creature-reverse-timer-init get-value()
  gset! creature-reverse-timer get-value()
  gset! biter-delay-frame get-value()
  gset! biters-wait-frame get-value()
  gset! replicator-delay-frame get-value()
  gset! replicators-wait-frame get-value()
  ;;
  ; load field
  define x
  define y 0
  while {y <> field-height}
    {x := 0}
    while {x <> field-width}
      {field[field-index-at(x y)] := get-value()}
      {field-time[field-index-at(x y)] := {get-value() + tick}}
      inc! x
    inc! y


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; cheats
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; exactly what is written on the tin.
(define cheat-harmless-mobiles #f)

define reset-cheats()
  gset! cheat-harmless-mobiles #f

;; exactly what it says on the tin.
define cheat-open-gate()
  if {not gate-open}
    then
      gset! gate-open #t
      gset! gate-flash #t


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; keys (not keyboard ones!) management
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; key #0: diamond key
(constant DIAMOND_KEY_IDX 0)

define has-key?(idx)
  if {{vector? key-count} and between?(idx 0 {vector-length(key-count) - 1})}
    {key-count[idx] <> 0}
    #f

define key-collected(idx)
  assert between?(idx 0 16) "invalid key index"
  if {vector? key-count}
    while {vector-length(key-count) <= idx}
      vector-push! key-count 0
    gset! key-count make-vector({idx + 1} 0)
  if {idx = DIAMOND_KEY_IDX}
    {key-count[idx] := 1}
    {key-count[idx] := {key-count[idx] + 1}}

;; return #t if had a key
define key-used(idx)
  assert between?(idx 0 16) "invalid key index"
  cond
    {has-key? idx}
      ; diamond key is persistent
      if {idx <> DIAMOND_KEY_IDX}
        {key-count[idx] := {key-count[idx] - 1}}
      #t
    else
      #f

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
#|
sticky keys implementation.

if "down" was processed by at least one frame, we should reset it on "up".
otherwise don't touch.

DOWN: gtick=9 frt=5 grt=6
UP: gtick=10 frt=-1 grt=150

here, key is processed on tick #9.
on frame #10 the key is released, yet the old sticky state is kept,
so it is processed on this frame too.

that is, if we processed keys at least once, they should "unstick",
and stick again only on key state toggle.

key state:
  bit 0: state to return on state query from an object
  bit 1: current state (real)
  bit 2: allow unstick flag

after processing a frame:
  bit 1 is copied to bit 0
  bit 2 is set

when the key is pressed:
  set bit 0
  set bit 1
  reset bit 2
when the key is released:
  reset bit 1
  if bit 2 is set, reset bit 0 and bit 2
|#


(constant KEY_LEFT  0)
(constant KEY_RIGHT 1)
(constant KEY_UP    2)
(constant KEY_DOWN  3)
(constant KEY_SNAP  4)
(constant KEY_DROP  5)

(define key-state (make-vector 6 0))


define reset-key-states()
  define idx 0
  while {idx <> vector-length(key-state)}
    {key-state[idx] := 0}
    inc! idx

;; after processing a frame:
;;   bit 1 is copied to bit 0
;;   bit 2 is set
define update-controls()
  define idx 0
  if sticky-keyboard-handiling
    while {idx <> vector-length(key-state)}
      {key-state[idx] := (if bit-set?(key-state[idx] 1) #b111 #b100)}
      inc! idx

;; when the key is pressed:
;;   set bit 0
;;   set bit 1
;;   reset bit 2
define key-down(idx)
  assert between?(idx 0 5) "invalid key code"
  if sticky-keyboard-handiling
    {key-state[idx] := #b011}
    {key-state[idx] := 1}

;; when the key is released:
;;   reset bit 1
;;   if bit 2 is set, reset bit 0 and bit 2
define key-up(idx)
  assert between?(idx 0 5) "invalid key code"
  if sticky-keyboard-handiling
    let <* v key-state[idx] *>
      {key-state[idx] := (if bit-set?(v 2) #b000 bit-reset(v 1))}
    {key-state[idx] := 0}

define key-pressed?(idx)
  assert between?(idx 0 5) "invalid key code"
  bit-set?(key-state[idx] 0)

define key-reset(idx)
  assert between?(idx 0 5) "invalid key code"
  {key-state[idx] := 0}


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; game field accessors.
;; all game field mutation (it is important!) should be done
;; via accessors. this is required for demo recording.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; return pair: (x . y)
define field-index->coords(idx)
  define wdt field-width
  {idx := {idx mod {wdt * field-height}}}
  cons {idx mod wdt} {idx div wdt}

define field-index->x(idx)
  define wdt field-width
  {idx := {idx mod {wdt * field-height}}}
  {idx mod wdt}

define field-index->y(idx)
  define wdt field-width
  {idx := {idx mod {wdt * field-height}}}
  {idx div wdt}


;; inlining `field-index-at` makes things faster by ~1 msec on 256x256 field.
;; totally doesn't worth it.
define field-index-at(x y)
  define wdt field-width
  {{{y mod field-height} * wdt} + {x mod wdt}}


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; demo replay helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define in-replay?()
  {bytevector? replay-data}

define replay-finished?()
  {{bytevector? replay-data} and {replay-data-pos >= bytevector-length(replay-data)}}

define get-replay-word()
  if {replay-data-pos = bytevector-length(replay-data)}
    -4
    else
      define n {replay-data[replay-data-pos] + {replay-data[{replay-data-pos + 1}] * 256}}
      gset! replay-data-pos {replay-data-pos + 2}
      if {n > 32767}
        dec! n 65536
      n

define get-replay-otype()
  define otype replay-otype-trans[get-replay-word()]
  assert {fixnum? otype}
  otype

define dump-replay-step()
  if {not replay-finished?()}
    then
      define re get-replay-word()
      printf "RE=%o\n" re
      cond
        {re = -1} ; end of frame; next 2 values: C64 PPRNG state at the end of the frame
          define v0 get-replay-word()
          define v1 get-replay-word()
          printf "END-OF-FRAME: pprng: %02X %02X\n" v0 v1
          dump-replay-step()
        {re = -2} ; end of cave (finished)
          printf "CAVE FINISHED\n"
          dump-replay-step()
        {re = -3} ; end of cave (died)
          printf "PLAYER DEAD\n"
          dump-replay-step()
        {re = -4} ; end of cave (aborted)
          printf "CAVE ABORTED\n"
          dump-replay-step()
        {re = -5} ; end of cave (skipped)
          printf "CAVE SKIPPED\n"
          dump-replay-step()
        {re = -8} ; player coords (x y) -- used for camera panning
          define px get-replay-word()
          define py get-replay-word()
          printf "PLAYER POS: %o, %o\n" px py
          dump-replay-step()
        {re = -16} ; magic wall active on this frame
          printf "MAGIC WALL ACTIVE\n"
          dump-replay-step()
        {re = -17} ; amoeba active on this frame
          printf "AMOEBA WALL ACTIVE\n"
          dump-replay-step()
        {re = -32} ; diamond collected
          printf "DIAMOND COLLECTED\n"
          dump-replay-step()
        {re = -33} ; score change (arg: delta)
          printf "ADD SCORE: %o\n" get-replay-word()
          dump-replay-step()
        {re = -42} ; exit opened
          printf "GATE OPENED\n"
          dump-replay-step()
        {re = -43} ; was explosion
          printf "WAS EXPLOSION\n"
          dump-replay-step()
        {re = -44} ; slime pprng was inited (no args)
          printf "SLIME PPRNG INITED\n"
          dump-replay-step()
        {re = -46} ; play sound (arg: sndid)
          printf "PLAY SOUND WITH ID: %o\n" otype-name(get-replay-otype())
          dump-replay-step()
        {re = -60} ; play object sound (arg: otype)
          printf "PLAY OBJECT SOUND: %o\n" otype-name(get-replay-otype())
          dump-replay-step()
        {re = -61} ; play object walk sound (arg: otype)
          printf "PLAY OBJECT WALK SOUND: %o\n" otype-name(get-replay-otype())
          dump-replay-step()
        {re = -62} ; play object push sound (arg: otype)
          printf "PLAY OBJECT PUSH SOUND: %o\n" otype-name(get-replay-otype())
          dump-replay-step()
        {re = -63} ; play object fall sound (arg: otype)
          printf "PLAY OBJECT FALL SOUND: %o\n" otype-name(get-replay-otype())
          dump-replay-step()
        {re = -69} ; player hatched
          printf "PLAYER HATCHED\n"
          dump-replay-step()
        else ; idx otype
          assert between?(re 0 {{field-width * field-height} - 1})
          define idx re
          define otype get-replay-otype()
          define x {idx mod field-width}
          define y {idx div field-width}
          printf "FIELD CHANGE AT (%o,%o): %o\n" x y otype-name(otype)
          dump-replay-step()

define set-replay-data(data otype-trans)
  assert {bytevector? data} "invalid replay data"
  if {bytevector? game-replay}
    bytevector-resize! game-replay 0
  if {{bytevector? replay-data} and {not {replay-data eq? data}}}
    bytevector-resize! replay-data 0
  if {{vector? replay-otype-trans} and {not {replay-otype-trans eq? otype-trans}}}
    vector-resize! replay-otype-trans 0
  gset! game-replay #nil
  gset! game-replay-pos 0
  gset! replay-data data
  gset! replay-data-pos 0
  gset! replay-otype-trans otype-trans
  ;; setup field
  assert {get-replay-word() = -666} "invalid replay start"
  assert {get-replay-word() = field-width} "invalid replay field width"
  assert {get-replay-word() = field-height} "invalid replay field height"
  define x
  define y
  define otype
  iterate
    init {y := 0}
    repeat {y <> field-height}
      iterate
        init {x := 0}
        repeat {x <> field-width}
          {otype := get-replay-word()}
          assert between?(otype 0 {::etype:_MAX_VALUE_ - 1})
          {field[field-index-at(x y)] := otype}
          inc! x
        else #void
      inc! y
    else #void
  ;;
  define rr get-replay-word()
  if {rr = -8}
    then
      gset! player-x get-replay-word()
      gset! player-y get-replay-word()
      {rr := get-replay-word()}
  assert {rr = -1} "invalid replay start"
  car-set! game-pprng get-replay-word()
  cdr-set! game-pprng get-replay-word()
  ;;
  ;define xxpos replay-data-pos
  ;dump-replay-step()
  ;gset! replay-data-pos xxpos


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; replay recording helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define wipe-replay()
  if {bytevector? game-replay}
    then
      printf "wiped replay size: %,d\n" game-replay-pos
      bytevector-resize! game-replay 0
  gset! game-replay #nil
  gset! game-replay-pos 0
  if {bytevector? replay-data}
    bytevector-resize! replay-data 0
  gset! replay-data #nil
  gset! replay-data-pos 0

define report-replay-size()
  if {bytevector? game-replay}
    printf "replay size: %,d\n" game-replay-pos

define replay-put-word(n)
  assert {{fixnum? n} and between?(n -32768 32767)} "invalid replay number"
  if {bytevector? game-replay}
    then
      define rpp game-replay-pos
      if {{rpp + 2} >= bytevector-length(game-replay)}
        bytevector-resize! game-replay {rpp + {1024 * 256}}
      {game-replay[rpp] := bit-and(n #xFF)}
      {game-replay[{rpp + 1}] := bit-and(bit-shr(n 8) #xFF)}
      gset! game-replay-pos {rpp + 2}


define replay-put-change(x y otype)
  replay-put-word field-index-at(x y)
  replay-put-word otype


define replay-put-frame-end()
  replay-put-word -1
  replay-put-word car(game-pprng)
  replay-put-word cdr(game-pprng)


define replay-put-play-sound(soundid)
  case soundid
    (GD_S_EXPLOSION)
      replay-put-word -46
      replay-put-word 1
    (GD_S_GHOST_EXPLOSION)
      replay-put-word -46
      replay-put-word 2
    (GD_S_BOMB_EXPLOSION)
      replay-put-word -46
      replay-put-word 3
    (GD_S_VOODOO_EXPLOSION)
      replay-put-word -46
      replay-put-word 4
    else
      printf "replay-play-sound: unknown id: %o\n" soundid

define start-recording()
  gset! game-replay make-bytevector(65537)
  gset! game-replay-pos 0
  ;; save playfield
  replay-put-word -666
  replay-put-word field-width
  replay-put-word field-height
  define x
  define y
  iterate
    init {y := 0}
    repeat {y <> field-height}
      iterate
        init {x := 0}
        repeat {x <> field-width}
          replay-put-word field[field-index-at(x y)]
          inc! x
        else #void
      inc! y
    else #void
  ;;
  replay-put-word -8
  replay-put-word max(0 player-x)
  replay-put-word max(0 player-y)
  ;;
  replay-put-frame-end()


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; more game field accessors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define field-time-ref(x y)
  field-time[field-index-at(x y)]

;define field-time-set!(x y time)
;  {field-time[field-index-at(x y)] := time}

define field-deactivate(x y)
  {field-time[field-index-at(x y)] := tick}


;; return object type
define field-otype-ref(x y)
  {field[field-index-at(x y)]}

;; set object type
define field-set!(x y otype)
  define idx field-index-at(x y)
  define oval field[idx]
  if (and {not {oval eq? etype:O_LAVA}}
          {not {oval eq? otype}})
    then
      {field[idx] := otype}
      replay-put-change x y otype

define field-forced-set!(x y otype)
  define idx field-index-at(x y)
  if {not {field[idx] eq? otype}}
    then
      {field[idx] := otype}
      replay-put-change x y otype

;; return entity definition
define field-edef-ref(x y)
  {entity-defs[field[field-index-at(x y)]]}

;; return entity object
define field-eobj-ref(x y)
  {entity-objects[field[field-index-at(x y)]]}

;; get image index to render
define field-image-index-ref(x y)
  define def field-edef-ref(x y)
  define otype edef:index(def)
  cond
    {otype = etype:O_MAGIC_WALL}
      if magic-walls-active?()
        (edef:game-image def)
        if game:option-ref('reveal-magic-walls)
          abs(edef:game-image(def))
          (edef:inactive-image def)
    {{otype = etype:O_INBOX} or {otype = etype:O_OUTBOX}}
      if {odd? tick}
        (edef:game-image def)
        (edef:inactive-image def)
    {{otype = etype:O_PRE_OUTBOX} or {otype = etype:O_PRE_INVIS_OUTBOX}}
      if option-ref('reveal-pre-outbox)
        368 ;; sorry!
        (edef:game-image def)
    {{otype = etype:O_H_EXPANDING_WALL} or {otype = etype:O_V_EXPANDING_WALL} or
     {otype = etype:O_EXPANDING_WALL}}
      if option-ref('respect-expanding-wall-looks-like)
        (edef:game-image entity-defs[option-ref('expanding-wall-looks-like)])
        (edef:game-image def)
    {otype = etype:O_DIRT}
      if option-ref('respect-dirt-looks-like)
        (edef:game-image entity-defs[option-ref('dirt-looks-like)])
        (edef:game-image def)
    else
      (edef:game-image def)

;; is object at the given coords has the given property?
define field-prop-at?(propname x y)
  edef:prop? field-edef-ref(x y) propname

;; is object at the given coords has the given property?
;; pair of #f
define field-get-prop-at(propname x y)
  edef:get-prop field-edef-ref(x y) propname


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; spawners
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; used by map flattener
define spawn-at-internal(x y otype)
  define delayed {negative? otype}
  {otype := abs(otype)}
  define def {entity-defs[otype]}
  if {not delayed}
    if {option-ref('short-explosions) and edef:prop?(def 'P_EXPLOSION_FIRST_STAGE)}
      {def := entity-defs[{otype + 1}]}
  field-forced-set! x y otype
  ;printf "otype=%o; def=%o\n" otype def
  if delayed
    then
      define old-tick tick
      gset! tick {old-tick + 1} ;; hack: will be spawned inactive
      ;field-set! x y otype
      field-deactivate x y
      gset! tick old-tick
    else
      field-deactivate x y
  def

;; spawn entity
define spawn-at(x y otype)
  ;assert {otype >= 0} string-append("negative otype in \"spawn-at\": " number->string(otype))
  field-set! x y abs(otype)
  field-deactivate x y

define spawn-at-forced(x y otype)
  ;assert {otype >= 0} "negative otype in \"spawn-at-forced\""
  field-forced-set! x y abs(otype)
  field-deactivate x y

;; move object (spawn a new one), leave empty space at the old position
define spawn-move-at(ox oy nx ny otype)
  spawn-at ox oy etype:O_SPACE
  spawn-at nx ny otype

;; move object, leave empty space at the old position.
;; do not inline this: it is easier to record replays without inlining.
define move-obj(ox oy nx ny)
  spawn-move-at ox oy nx ny field-otype-ref(ox oy)


;; create new game with the field of the given size.
;; the field is filled with emptiness.
define new-cave(wdt hgt opt-override)
  gset! cave-options-override opt-override
  if {{boolean? wdt} or {null? wdt}}
    {wdt := option-ref('width)}
  if {{boolean? hgt} or {null? hgt}}
    {hgt := option-ref('height)}
  assert between?(wdt 1 max-field-width) "invalid field width"
  assert between?(hgt 1 max-field-height) "invalid field height"
  gset! field make-vector({wdt * hgt} etype:O_SPACE)
  gset! field-time make-vector({wdt * hgt} 0)
  gset! field-width wdt
  gset! field-height hgt
  gset! tick 0
  gset! last-dir-left #t
  gset! last-move-hdir #nil
  reset-options()
  reset-flags()
  reset-key-states()
  gset! game-pprng make-pprng-2seeds(0 #x1E)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; explosions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; spawn an explosion.
;; objects should use this API, otherwise
;; explosions will not be properly recorded.
define explode-at(x y otype-expl)
  ;printf "x=%o; y=%o; otype=%o; nex=%o (%o) : %o\n"
  ;  x
  ;  y
  ;  field-otype-ref(x y)
  ;  field-prop-at?('P_NON_EXPLODABLE x y)
  ;  edef:props field-edef-ref(x y)
  ;  assq 'P_NON_EXPLODABLE edef:props(field-edef-ref(x y))
  if {not field-prop-at?('P_NON_EXPLODABLE x y)}
    let <* otx field-otype-ref(x y) \\ exploded #f *>
      cond
        {otx = etype:O_VOODOO}
          if option-ref('voodoo-any-hurt-kills-player)
            gset! voodoo_touched #t
          if option-ref('voodoo-disappear-in-explosion)
            then
              spawn-at x y otype-expl
              {exploded := #t}
            else
              spawn-at x y etype:O_TIME_PENALTY  ;; voodoo -> time penalty
        {{otx = etype:O_NITRO_PACK} or {otx = etype:O_NITRO_PACK_F}}
          ;; nitro pack -> triggered
          spawn-at x y etype:O_NITRO_PACK_EXPLODE
          {exploded := #t}
        else
          spawn-at x y otype-expl
          {exploded := #t}
      if {exploded and {not was-explosion}}
        then
          gset! was-explosion #t
          replay-put-word -43

;; spawn an explostion, special handling for voodoo dolls.
;; objects should use this API, otherwise
;; explosions will not be properly recorded.
define explode-at-voodoo-special(x y otype-expl)
  if {not field-prop-at?('P_NON_EXPLODABLE x y)}
    let <* otx field-otype-ref(x y) \\ exploded #f *>
      if entity-objects[otx][player?:]
        gset! player-must-explode #t
      cond
        {otx = etype:O_VOODOO}
          if option-ref('voodoo-disappear-in-explosion)
            then
              if option-ref('voodoo-any-hurt-kills-player)
                gset! voodoo_touched #t
              spawn-at x y otype-expl
              {exploded := #t}
        else
          spawn-at x y otype-expl
          {exploded := #t}
      if {exploded and {not was-explosion}}
        then
          gset! was-explosion #t
          replay-put-word -43


;; used in "step()"
define update-magic-walls-timer()
  cond
    {{true? magic-wall-state} and magic-timer-active}
      if {(gset! magic-wall-timer {magic-wall-timer - 1}) <= 0}
        gset! magic-wall-state #nil
    else
      #void

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; object API: helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; increment (or decrement) score.
define add-score(amount)
  if {amount <> 0}
    then
      replay-put-word -33
      replay-put-word amount
  gset! cave-score {cave-score + amount}


;; get current diamond score (price).
define diamond-price()
  (if {diamond-count < diamonds-required} diamond-value extra-diamond-value)

;; called by the diamond object when the diamond is collected.
define diamond-collected()
  replay-put-word -32
  add-score diamond-price()
  gset! diamond-count {diamond-count + 1}
  if {diamond-count = diamonds-required}
    then
      replay-put-word -42
      gset! gate-open #t
      gset! gate-flash #t


;; continuously called by the magic wall object
;; when the magic wall is active.
define activate-magic-walls()
  cond
    {false? magic-wall-state}
      gset! magic-wall-state #t
      gset! magic-wall-timer magic-wall-initial-timer
    else
      #f

;; check if magic walls were activated.
define magic-walls-active?()
  {true? magic-wall-state}

;; check if there were any active magic walls at the processed game frame.
define seen-active-magic-walls?()
  {{true? magic-wall-state} and {positive? magic-walls-count}}

;; check if there were any active amoeba at the processed game frame.
define seen-active-amoeba?()
  {{positive? amoeba-count} or {positive? amoeba-2-count}}


;; public API.
;; called by the amoeba object.
;; note that amoeba directly modifies "amoeba-state" and "amoeba-can-spread".
define amoeba-processed()
  gset! amoeba-count {amoeba-count + 1}

;; public API.
;; called by the amoeba-2 object.
;; note that amoeba directly modifies "amoeba-2-state" and "amoeba-2-can-spread".
define amoeba-2-processed()
  gset! amoeba-2-count {amoeba-2-count + 1}

;; public API.
;; called by the magic wall object.
define magic-wall-scanned()
  gset! magic-walls-count {magic-walls-count + 1}


;; public API.
;; used by the slime object.
define init-slime-pprng-seed()
  cond
    {not slime-pprng-seed-inited}
      gset! slime-pprng-seed-inited #t
      define ss option-ref('level-slime-seed-c64)
      if {ss >= 0} set-pprng-seed-u16(ss)
    else
      #void


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; player state management
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; public API.
;; called by the hatch explosion object.
define player-hatch()
  cond
    {not player-hatched}
      play-sound-with-id 'GD_S_CRACK
      gset! player-hatched #t
      replay-put-word -69
      if option-ref('creatures-direction-auto-change-on-start)
        gset! creatures-backwards {not creatures-backwards}
      gset! magic-timer-active #t
      ;; if the number of required diamons is not positive, calculate it
      if {diamonds-required <= 0}
        gset! diamonds-required max(0 {diamonds-required + count-diamonds()})
      if {zero? diamonds-required}
        then
          replay-put-word -42
          gset! gate-open #t
          gset! gate-flash #t
    else
      #void


;; internal API.
define set-current-plr-coords(x y real)
  gset! player-x x
  gset! player-y y
  gset! player-inbox {not real}


;; public API.
;; called by the player or by the inbox to notify
;; the engine about player position. may be called
;; several times if there are several players/inboxes.
;; if "real" is #f, this is inbox.
define player-found(x y real)
  if {{not real} or field-eobj-ref(x y)[player?:]}
    then
      cond
        {zero? player-count}
          set-current-plr-coords(x y real)
        option-ref('active-is-first-found)
          ;; should be upper, or at the left of the last found
          if {{y < player-y} or {{y = player-y} and {x < player-x}}}
            set-current-plr-coords(x y real)
        else
          ;; should be lower, or at the right side
          if {{y > player-y} or {{y = player-y} and {x > player-x}}}
            set-current-plr-coords(x y real)
      gset! player-count {player-count + 1}
      if {real and {not player-hatched}}
        player-hatch()


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; internal "step()" helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; called only if there is some amoeba.
;; used in "step()"
define update-amoeba()
  ;; need to activate amoeba timer?
  if {{not amoeba-timer-active} and
      {player-hatched or {not option-ref('amoeba-timer-wait-for-hatching)}} and
      {{game:amoeba-state eq? 'AWAKE} or {game:amoeba-2-state eq? 'AWAKE} or
       option-ref('amoeba-timer-started-immediately)}}
    gset! amoeba-timer-active #t
  ;; process amoeba timer
  if {amoeba-timer-active and {gset!(amoeba-time {amoeba-time - 1}) <= 0}}
    then
      gset! amoeba-time 0
      gset! amoeba-growth-prob amoeba-fast-growth-prob
      gset! amoeba-2-growth-prob amoeba-2-fast-growth-prob
  if {game:amoeba-state eq? 'AWAKE}
    then
      if {amoeba-count >= amoeba-threshold}
        gset! game:amoeba-state 'TOO-BIG
      if {{positive? amoeba-count} and {not amoeba-can-spread}}
        gset! game:amoeba-state 'ENCLOSED
  if {game:amoeba-2-state eq? 'AWAKE}
    then
      if {amoeba-2-count >= amoeba-2-threshold}
        gset! game:amoeba-2-state 'TOO-BIG
      if {{positive? amoeba-2-count} and {not amoeba-2-can-spread}}
        gset! game:amoeba-2-state 'ENCLOSED


;; change creatures direction.
;; used in "step()"
define update-cdir-timer()
  if {zero? gset!(creature-reverse-timer {creature-reverse-timer - 1})}
    then
      gset! creature-reverse-timer creature-reverse-timer-init
      gset! creatures-backwards {not creatures-backwards}


;; if the number of required diamonds is not positive,
;; count them with this procedure on player hatching.
define count-diamonds()
  define otype
  define count 0
  define ent
  define idx {field-width * field-height}
  while {idx <> 0}
    dec! idx
    {otype := field[idx]}
    {ent := entity-defs[otype]}
    if edef:prop?(ent 'P_DIAMOND)
      inc! count
  count


;; internal API.
;; player history is used for chasing boulders.
define update-player-history()
  if {not {vector? player-history-x}}
    then
      gset! player-history-x make-vector(16 player-x)
      gset! player-history-y make-vector(16 player-y)
  ;; player-history-stpos is the position to store
  {player-history-x[player-history-stpos] := player-x}
  {player-history-y[player-history-stpos] := player-y}
  ;; advance position
  gset! player-history-stpos {{player-history-stpos + 1} mod vector-length(player-history-x)}
  ;; now reading will get the latest element again

;; public API.
;; used by the chasing boulder object.
define player-chase-x()
  player-history-x[player-history-stpos]

;; public API.
;; used by the chasing boulder object.
define player-chase-y()
  player-history-y[player-history-stpos]


;; internal API.
;; used by "step()".
define update-biters-timer()
  if {zero? biters-wait-frame}
    gset! biters-wait-frame biter-delay-frame
    gset! biters-wait-frame {biters-wait-frame - 1}

;; internal API.
;; used by "step()".
define update-replicator-timer()
  if {zero? replicators-wait-frame}
    gset! replicators-wait-frame replicator-delay-frame
    gset! replicators-wait-frame {replicators-wait-frame - 1}


;; for time reports in SDL window title.
(define step-time-report make-running-average())


;; internal API. used by "step()".
;; replay demo events until end of frame reached.
;; "end-of-frame" object is processed too.
define step-replay()
  if {not replay-finished?()}
    then
      define re get-replay-word()
      ;printf "  re: %o\n" re
      cond
        {re = -1} ; end of frame; next 2 values: C64 PPRNG state at the end of the frame
          car-set! game-pprng get-replay-word()
          cdr-set! game-pprng get-replay-word()
        {re = -2} ; end of cave (finished)
          gset! cave-finished #t
          step-replay()
        {re = -3} ; end of cave (died)
          gset! player-dead #t
          step-replay()
        {re = -4} ; end of cave (aborted)
          gset! player-dead #t
          step-replay()
        {re = -5} ; end of cave (skipped)
          gset! player-dead #t
          step-replay()
        {re = -8} ; player coords (x y) -- used for camera panning
          gset! player-x get-replay-word()
          gset! player-y get-replay-word()
          step-replay()
        {re = -16} ; magic wall active on this frame
          gset! magic-wall-state #t
          gset! magic-walls-count 1
          step-replay()
        {re = -17} ; amoeba active on this frame
          gset! amoeba-count 1
          step-replay()
        {re = -32} ; diamond collected
          gset! diamond-count {diamond-count + 1}
          step-replay()
        {re = -33} ; score change (arg: delta)
          gset! cave-score {cave-score + get-replay-word()}
          step-replay()
        {re = -42} ; exit opened
          gset! gate-open #t
          gset! gate-flash #t
          step-replay()
        {re = -43} ; was explosion
          gset! was-explosion #t
          step-replay()
        {re = -44} ; slime pprng was inited (no args)
          gset! slime-pprng-seed-inited #t
          step-replay()
        {re = -46} ; play sound (arg: sndid)
          define sndid get-replay-word()
          cond
            {sndid eq? 1}
              play-sound 'GD_S_EXPLOSION
            {sndid eq? 2}
              play-sound 'GD_S_GHOST_EXPLOSION
            {sndid eq? 3}
              play-sound 'GD_S_BOMB_EXPLOSION
            {sndid eq? 4}
              play-sound 'GD_S_VOODOO_EXPLOSION
            else
              printf "REPLAY: unknown (-46) sound id: %o\n" sndid
          step-replay()
        {re = -60} ; play object sound (arg: otype)
          play-sound-of-otype get-replay-otype()
          step-replay()
        {re = -61} ; play object walk sound (arg: otype)
          play-walk-sound-of-otype get-replay-otype()
          step-replay()
        {re = -62} ; play object push sound (arg: otype)
          play-push-sound-of-otype get-replay-otype()
          step-replay()
        {re = -63} ; play object fall sound (arg: otype)
          play-fall-sound-of-otype get-replay-otype()
          step-replay()
        {re = -69} ; player hatched
          if {not player-hatched}
            play-sound-with-id 'GD_S_CRACK
          gset! player-hatched #t
          step-replay()
        else ; idx otype
          assert between?(re 0 {{field-width * field-height} - 1})
          define idx re
          define otype get-replay-otype()
          define x {idx mod field-width}
          define y {idx div field-width}
          field-forced-set! x y otype
          step-replay()


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; public API.
;; perform one game frame ("cave step").
define step()
  define gtick tick
  define ntick {gtick + 1}
  gset! tick ntick
  gset! was-explosion #f
  if in-replay?()
    then
      ;printf "*** FRAME ***\n"
      gset! magic-wall-state #f
      gset! magic-walls-count 0
      gset! amoeba-count 0
      step-replay()
    else
      define obj
      define otype
      define fld-idx 0
      define fld-wdt field-width
      define fld-size {field-width * field-height}
      define stt ticks-msec()
      ;; "cave-time" should be modified by the main driver
      ;gset! gate-flash #f ;; should be reset by the main driver
      gset! magic-walls-count 0
      gset! amoeba-count 0
      gset! amoeba-2-count 0
      gset! amoeba-can-spread #f
      gset! amoeba-2-can-spread #f
      gset! player-count 0
      gset! player-inbox #t
      ;;
      define check-bad-options()
        if {{positive? magic-walls-count} and {{positive? amoeba-count} or {positive? amoeba-2-count}} and
            game:option-ref('magic-wall-stops-amoeba)}
          then
            printf "ERROR: \"MagicWallProperties.convertamoeba=true\" is not supported!\n"
            gset! has-bad-objects #t
            ::option-remove! cave-options-override 'magic-wall-stops-amoeba
      ;;
      cond
        fast-step-loop
          while {fld-idx <> fld-size}
            if {field-time[fld-idx] <= gtick}
              then
                {field-time[fld-idx] := ntick}
                {otype := field[fld-idx]}
                entity-objects[otype][tick: otype {fld-idx mod fld-wdt} {fld-idx div fld-wdt}]
            inc! fld-idx
        else
          define x 0
          define y 0
          while {y <> field-width}
            {x := 0}
            while {x <> field-height}
              if {field-time-ref(x y) <= gtick}
                then
                  field-deactivate x y
                  {otype := field[fld-idx]}
                  field-eobj-ref(x y)[tick: otype x y]
              inc! x
            inc! y
      ;;
      if {zero? player-count}
        if {gset!(player-missing-frames {player-missing-frames + 1}) > 8}
          gset! player-dead #t
        gset! player-missing-frames 0
      if voodoo-touched
        gset! kill-player #t
      ;;
      check-bad-options()
      update-player-history()
      update-magic-walls-timer()
      if {{positive? amoeba-count} or {positive? amoeba-2-count}} update-amoeba()
      if creature-reverse-timer-active update-cdir-timer()
      update-biters-timer()
      update-replicator-timer()
      update-controls()
      step-time-report({ticks-msec() - stt})
      ;; player coords
      replay-put-word -8
      replay-put-word player-x
      replay-put-word player-y
      ;;
      replay-put-frame-end()
      if seen-active-magic-walls?()
        replay-put-word -16
      if seen-active-amoeba?()
        replay-put-word -17
      ;if {step-time-report() >= 12}
      ;  printf "***WARNING! cave processing time is %o msecs!\n" stt


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PRNG API (non-C64 PRNG)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
define random(max)
  ::random(max)

define random-range-incl(min max)
  {min + ::random({max - min - -1})}


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; predictable PRNG
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

define pprng?(pctx)
  {pair?(pctx) and
   fixnum?(car(pctx)) and fixnum?(cdr(pctx)) and
   between?(car(pctx) 0 255) and between?(cdr(pctx) 0 255)}

define ?pprng(pctx)
  assert pprng?(pctx) "invalid PPRNG context"

define make-pprng-2seeds(seed1 seed2)
  assert fixnum?(seed1) "PPRNG seed1 should be a fixnum"
  assert fixnum?(seed2) "PPRNG seed2 should be a fixnum"
  assert between?(seed1 0 255) "PPRNG seed1 should be a 8-bit unsigned"
  assert between?(seed2 0 255) "PPRNG seed2 should be a 8-bit unsigned"
  cons(seed1 seed2)

define make-pprng-u16(seed)
  if {negative? seed} {seed := game:random(65536)}
  make-pprng-2seeds bit-and(bit-shr(seed 8) #xFF) bit-and(seed #xFF)

define make-pprng-u8(seed)
  if {negative? seed} {seed := game:random(256)}
  make-pprng-2seeds 0 seed

define pprng-next(pctx)
  ?pprng pctx
  define temp1
  define temp2
  define result
  define carry
  ;;
  {temp1 := bit-shl(bit-and(car(pctx) #x0001) 7)}
  {temp2 := bit-and(bit-shr(cdr(pctx)) #x007F)}
  {result := {cdr(pctx) + bit-shl(bit-and(cdr(pctx) #x0001) 7)}}
  {carry := bit-shr(result 8)}
  {result := {bit-and(result #x00FF) + carry + #x13}}
  {carry := bit-shr(result 8)}
  cdr-set! pctx bit-and(result #x00FF)
  {result := {car(pctx) + carry + temp1}}
  {carry := bit-shr(result 8)}
  {result := {bit-and(result #x00FF) + carry + temp2}}
  ;printf "res=%o\n" bit-and(result #x00FF)
  car-set! pctx bit-and(result #x00FF)


define set-pprng-seed-u8(seed)
  gset! game-pprng make-pprng-u8(seed)

define set-pprng-seed-u16(seed)
  gset! game-pprng make-pprng-u16(seed)

define pprng-u8()
  pprng-next game-pprng


;; load "flattener"
(load "bd-42-game-10-bdcff-flatten.lsp")


(end-module 'game)
