Bitmage - js1k 2017

For js1k 2017 I created a small Roguelike game where you play a magic user who is being chased by green slimy things. Your goal is to collect blue mana while you try to not be zapped. Using mana you can blast digestive enzymes out to remove everything in you vicinity: dungeon walls, mana and green slime.

At least that is my attempt to create some kind of story to hype the game. You really are just moving a yellow square around in a slightly bigger grayish square :-)

Spoilers and Game Mechanics

I spent the most time on trying to make the game become interesting through some simple mechanics. One is having the best score persist across browser reloads thanks to localstorage. This adds a little excitement since you may be able to beat your or your friends previous best score.

Interaction with enemies became more interesting than I had originally imagined. Unlike the player they can move diagonally and they are only dangerous if passed from one of the diagonals. If the player confronts an enemy by moving horizontally or vertically directly into it the enemy vanishes and 3 points are added. This makes it possible to survive for some time even if you do not have any mana by pressing space to pass time and hope for a chance to confront and enemy.

As you progress to higher levels the number of enemies increase and there are fewer walls making it more difficult to survive. The focus of the game is changed slightly and it becomes increasingly important to have collected enough mana to get enemies out of the way.

All in all I am happy about how it turned out. The gameplay is even mildly interesting if you can abstract from the horrid graphics and the monotony,… hehe.


You can Try out Bitmage at JS1k.com.

Note that if the browser window becomes too small the window will scroll when arrow keys are pressed which is annoying. Reloading the browser will resize the game to fit the screen again.

The code

Thanks to the google closure compiler and RegPack I could get away without brushing up too much on my code golfing skills. Maybe next time. Code golfing is a lot of fun.

The initial version of this code used ES6 arrow functions and destructuring in an attempt to save bytes, but after applying compression the total file size became smaller when assigning variables and writing function instead of =>. Oh well.

The best way to save bytes is doing it by hand. I did have a plan for this but ran out of time. It would also have been nice to add the predictable random number generator back in again. It created some nicer mazes.


// Bitmage - js1k 2017
//
// You are a yellow bitmage exploring dungeons.  You love finding delicious blue
// mana that you consume hoping to grow stronger.  As a member of the
// kingdom of fungi you are able to secrete an acidic venom that digests all
// material around you. A useful skill in these areas.
//
// Lately the green slime have been getting on your nerves.  They do not obey
// the same rules as you and too often they are hostile.  It is time to teach
// them a lesson!
//
//   "Green slime are the most tricky part about being a bitmage.  They follow
//    me everywhere I go, impossible to get rid of.  I am not sure they want to
//    harm me but experience has taught me to take no chances.  Sometimes
//    confronting them head on is the best approach."  
//
// Objective
//
//   Avoid crossing the green slime, collect mana
//   and see if you can get the highest score.
//
// Controls
//
//   Arrows keys to move
//   Space to cast digestive secretion spell
//   Enter to continue
//
// Scoring
//
//   Find mana:        3 points
//   Confront slime:   5 points
//   Blast slime       9 points
//   Complete level:  20 points
//
// TODO
//
// * Better AI
// * wrap around properly
// * Deterministic randomness
// * Place enemies better
// * Colors
// * Save more bytes

var win = window
var height = win.innerHeight
var w = 40
var g = height / w | 0
var x
var y
var dx
var dy
var i
var j
var ii
var jj
var pos
var died = w
var points
var bomb
var baddies
var mana = 3
var completed = 1
var level = 0
//            floor   boom     wall   enemy    died    bomb    player
var colors = ['#333', '#555', '#222', '#384', '#B44', '#55d', '#A82']
var dungeon
var bg = dungeon = []

function r1 () {
  return 200 * (Math.random() - 0.5) | 0
}

points = bomb = 0

var args = [w, -w, -1, 1, 0]
var moved

const FLOOR = 0
const BOOM = 1
const WALL = 2
const ENEMY = 3
const DIED = 4
const BOMB = 5
const PLAYER = 6

function xx (ev) {
  // Player
  ev = ev.which
  dx = ev == 37 ? -1 : ev == 39 ? 1 : 0
  dy = ev == 38 ? -1 : ev == w ? 1 : 0
  bomb = ev == 32 ? 1 : 0

  if (completed | died) {
    if (completed) level ++
    else level = 1, mana = 3
    // make dungeon
    i = w * w
    for (;i--;) dungeon[i] = r1() > 9 + 9 * level ? WALL : FLOOR
    x = y = w / 2

    // place baddies and bombs
    bomb = 2 + level
    baddies = bomb + level
    i = w * w
    // at higher levels bombs and baddies become more separated
    for (;i--;) if (dungeon[i] < WALL) {
      if (baddies && r1() > 99 - level) baddies--, dungeon[i] = ENEMY
      if (bomb && r1() > 99 - level) bomb--, dungeon[w * w - i] = BOMB
    }
    dungeon[x + y * w] = PLAYER
    if (died != w && ev != 13) return
    completed = died = 0
    bg = []
  }

  pos = x + dx + (y + dy) * w
  if (bomb + dx + dy && pos > 0 && pos < w*w && dungeon[pos] != WALL) {
    if (dungeon[pos] == BOMB) points += 3, mana ++
    if (dungeon[pos] == ENEMY) points += 5
    if (bomb && mana) {
      mana--
      jj = 20
      for (;jj--;)
          ii = pos + args[jj % 4] + args[jj % 5],
          points += dungeon[ii] == ENEMY ? 9 : 0,
          bomb = bg[ii] = dungeon[ii] = BOOM
    }
    ii = x + y * w
    dungeon[ii] = bg[ii] || FLOOR
    dungeon[pos] = PLAYER
    x = x + dx
    y = y + dy
    moved = {}
    for (i = w; i--;) {
      for (j = w; j--;) {
        if (dungeon[j + i * w] == ENEMY & !moved[j + i * w]) {
          // move enemy
          dx = x - j
          dy = y - i

          if (dx * dx + dy * dy < 2) dungeon[x + y * w] = died = DIED

          pos = j + i * w

          dy = dy > 1 ? 1 : dy < -1 ? -1 : 0
          dx = dx > 1 ? 1 : dx < -1 ? -1 : 0

          jj = w
          ii = j + dx + (i + dy) * w
          while (jj-- && dungeon[ii] > BOOM) ii = pos + args[r1() % 4]
          dungeon[pos] = bg[pos] || FLOOR
          dungeon[ii] = ENEMY
          moved[ii] = 1
        }
      }
    }
    // All baddies killed?
    completed = !moved[ii]
  }

  // Paint stuff
  for (i = w; i--;) {
    for (j = w; j--;) {
      c.fillStyle = colors[dungeon[j + i * w]];
      c.fillRect(j * g, i * g, g, g);
    }
  }
  c.fillStyle = colors[6]
  c.font = (2 * g | 0) + 'px Arial'

  if (points >= ~~win.localStorage.h) win.localStorage.h = points
  c.fillText('Mana ' + mana + '  Level ' + level + '  Score ' + points +
             '  Best ' + win.localStorage.h, 9, w * g - 9)

  if (died) points = 0, c.fillText('Sad', g * w / 3, g * w / 2)
  else if (completed) c.fillText('Joy', g * w / 3, g * w / 2), points += 20
}

d.onkeydown = xx
xx(d)