Click here to play the fullscreen version

Idle goldmine

"Idle" or "clicker" games became hugely popular around 2013, so we're a little late to the party, but we can still have some fun creating one.

The idea of these games is to give the player a reward for a simple action (such as a click). Some people enjoy these games, others prefer not to waste their precious time. I'm not judging.
In our case the player will be operating a gold mine and his reward for clicking will be a new voxel-style worker. This "reward" will set you back $5.
Each worker tirelessly digs the gold nuggets and transports them to chests, where an automated lift picks them up and takes them to another chest on the surface.
From there another crew transports the nuggets to the warehouse and the player gets $1 for each (apparently the gold market is not very hot right now).
For only $20, you can open a new shaft.
The game, like human greed, never ends, but it gets old pretty soon: you open all the shafts and fill them with workers. At that point, you can only idly watch your wealth grow or check out one of the tutorials below.

Here's how it works (full code below):

[1-7] Set up the HTML5 canvas and its font.
[8-27] Images
[29-30] Shaft dimensions
[32] $5 to hire a new worker
[33] max 3 workers per shaft
[34] $20 to open a new shaft (what a bargain!)
[35] 10 shafts max - that's how many can fit on our canvas
[36-37] dimensions of images
[38] each animation sequence has 20 frames
[39] the ratio of horizontal and vertical stretch of the canvas to fit the device screen
[40] score is your current cash
[41] init the array of shafts
[42] the current animation frame (start with 0)

[43-81] the lift object:
[44] initial speed
[45-59] When the lift arrives at a shaft:
[47-52] if it's underground, move the gold (if any) from the chest to the lift, empty the shaft chest, start the loading counter
[54-58] otherwise move the gold from the lift to the shaft chest
[60-67] Move the lift:
[61] change the y coordinate
[63-64] if the lift arrived at a shaft, execute the corresponding function
[65-66] if it reached the top or the bottom, reverse the speed
[68-80] Draw and update the lift:
[69] Draw the bitmap
[70] Show the chest contents
[71-75] If the loading counter is on, draw the arrow in the current direction
[76] and decrease the counter
[77-78] otherwise move the lift.

[83-100] The shaft
[84] start without any workers
[85] and an empty chest
[86] closed by default (we'll open it later)
[87-93] Draw the shaft:
[88-92] If there is room for more workers, show the "hire worker" button (active or inactive, depending on whether you have enough cash)
[94-98] Opening a new shaft:
[95] flip to "open"
[96] increase the counter of open shafts
[97] automatically hire the first worker
[98] and pay for him.

[102-154] Workers
[103] randomly start somewhere close to the lift
[104] facing right
[107-153] Worker logic:
[110-117] If he's digging:
[111] Increase the digging counter
[112] use the 5th row of the spritesheet:


[113] If he's been digging for 100 frames, finish digging and go to the lift.
[118-130] If he's going right:
[119] use the 1st (top) row of the spritesheet
[120] move right
[121] if he's close to the right hand side, start digging or (if on surface) deposit the load in the warehouse and go back to the lift
[131-146] If he's going left:
[132] Use the 2nd row of the spritesheet
[133] move left
[134-144] if he's close to the chest:
[135] turn around
[136-7] add the gold to the chest if underground
[138-143] if there's gold in the chest, grab it
[147-148] If on surface, go down two rows in the spritesheet (to reverse the direction he's facing). On the surface, he carries the nugget going left and is empty handed going right. Underground, it's the opposite.
[149-150] If on surface and empty-handed, use the 1st row.
[152] draw the frame from the spritesheet.
[156-169] Draw the screen:
[157] draw the background image for the open shafts
[158] show the score
[159-162] draw the open shafts
[165-168] draw the active/inactive "new shaft" button

[171-183] Main animation loop
[173-177] update each worker
[179-181] Increase the frame counter and reset if needed.
[182] Wait for the next animation frame.
[185-203] Initial values for the variables.
[205-214] In case the window is resized (eg. the mobile device is rotated), change the canvas style dimensions to fill the entire screen and calculate the updated x/y aspects.

[216-230] Click handling
[217-218] recalculate the coordinates taking the aspects into account
[219] Which shaft button was clicked?
[221-224] if a "hire worker" button was clicked, add a worker and pay for him
[225-227] otherwise open a new shaft

[232-242] When the window first loads:
[233-238] create the shafts and set the maximum number of workers
[235-236] surface can have 5 times more workers than shafts.


<html>
<body>
<canvas id='canvaswidth='600height='600style='position:absoluteleft:0top:0'></canvas>
<script>
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.font = 'bold 16px sans-serif';
const backgroundImg = new Image();
backgroundImg.src = 'background.png';
const liftImg = new Image();
liftImg.src = 'lift.png';
const arrowLeftImg = new Image();
arrowLeftImg.src = 'arrow.png';
const arrowRightImg = new Image();
arrowRightImg.src = 'arrow_right.png';
const spritesheetImg = new Image();
spritesheetImg.src = 'spritesheet.png';
const closeImg = new Image();
closeImg.src = 'close.png';
const workerActiveButtonsImg = new Image();
workerActiveButtonsImg.src = 'worker_active.png';
const workerInactiveButtonsImg = new Image();
workerInactiveButtonsImg.src = 'worker_inactive.png';
const shaftActiveButtonsImg = new Image();
shaftActiveButtonsImg.src = 'shaft_active.png';
const shaftInactiveButtonsImg = new Image();
shaftInactiveButtonsImg.src = 'shaft_inactive.png';
 
const shaftHeight = 60,
  shaftWidth = 450,
  liftWidth = 40,
  workerCost = 5,
  max_workers = 3,
  shaftCost = 20,
  max_shafts = 10,
  buttonWidth = 150,
  spriteSize = 60,
  max_frames = 19;
let aspectXaspectY;
let scoreshaftsOpen;
let shafts = [];
let frame = 0;
let lift = {
  speed: .5,
  arrivedAtShaftfunction(shaftNumber) {
    let shaft = shafts[shaftNumber];
    if (shaftNumber > 0) {
      if (shaft.chest > 0) {
        this.chest = this.chest + shaft.chest;
        shaft.chest = 0;
        this.loadingCounter = 100;
      }
    } else {
      if (this.chest > 0)
        this.loadingCounter = 100;
      shaft.chest = shaft.chest + this.chest;
      this.chest = 0;
    }
  },
  movefunction() {
    this.y = this.y + this.speed;
    let currentShaft = this.y / shaftHeight;
    if (currentShaft == Math.floor(currentShaft))
      this.arrivedAtShaft(currentShaft);
    if (this.y >= (shaftsOpen - 1) * shaftHeight || this.y <= 0)
      this.speed = -this.speed;
  },
  updatefunction() {
    context.drawImage(liftImg0this.y);
    context.fillText(this.chest10this.y + 25);
    if (this.loadingCounter) {
      if (this.y > 0)
        context.drawImage(arrowLeftImgliftWidth * .5this.y);
      else
        context.drawImage(arrowRightImgliftWidth * .5this.y);
      this.loadingCounter--;
    } else {
      this.move();
    }
  },
};
 
function Shaft() {
  this.workers = [];
  this.chest = 0;
  this.closed = true;
  this.draw = function(shaftNumber) {
    if (shafts[shaftNumber].workers.length < shafts[shaftNumber].max_workers)
      if (score >= workerCost)
        context.drawImage(workerActiveButtonsImgshaftWidthshaftHeight * shaftNumber);
      else
        context.drawImage(workerInactiveButtonsImgshaftWidthshaftHeight * shaftNumber);
  };
  this.open = function() {
    this.closed = false;
    shaftsOpen++;
    this.workers.push(new Worker());
    score = score - shaftCost;
  };
}
 
function Worker() {
  this.x = Math.floor(Math.random() * liftWidth);
  this.mode = 'goRight';
  this.counter = 0;
  this.load = 0;
  this.update = function(shaftNumber) {
    let row;
    switch (this.mode) {
      case 'dig':
        this.counter++;
        row = 4;
        if (this.counter > 100) {
          this.counter = 0;
          this.mode = 'goLeft';
        }
        break;
      case 'goRight':
        row = 0;
        this.x++;
        if (this.x >= shaftWidth - spriteSize * 1.5 + Math.random() * spriteSize) {
          if (shaftNumber > 0)
            this.mode = 'dig';
          else {
            score = score + this.load;
            this.load = 0;
            this.mode = 'goLeft';
          }
        }
        break;
      case 'goLeft':
        row = 1;
        this.x--;
        if (this.x <= 2 * liftWidth) {
          this.mode = 'goRight';
          if (shaftNumber > 0) {
            shafts[shaftNumber].chest = shafts[shaftNumber].chest + 1;
          } else {
            if (shafts[0].chest > 0) {
              this.load = 1;
              shafts[0].chest = shafts[0].chest - 1;
            }
          }
        }
        break;
    }
    if (shaftNumber == 0) {
      row = row + 2;
      if (this.load == 0 && this.mode == 'goRight')
        row = 0;
    }
    context.drawImage(spritesheetImgframe * spriteSizerow * spriteSizespriteSizespriteSizethis.xshaftNumber * shaftHeightspriteSizespriteSize);
  };
}
 
function drawScreen() {
  context.drawImage(backgroundImg00shaftWidth + buttonWidthshaftHeight * shaftsOpen00shaftWidth + buttonWidthshaftHeight * shaftsOpen);
  context.fillText(score30015);
  shafts.forEach((shaftshaftNumber) => {
    if (!shaft.closed) {
      context.fillText(shaft.chestliftWidth + 15shaftNumber * shaftHeight + 25);
      shaft.draw(shaftNumber);
    }
  });
  if (score >= shaftCost)
    context.drawImage(shaftActiveButtonsImgshaftWidthshaftHeight * (shaftsOpen));
  else
    context.drawImage(shaftInactiveButtonsImgshaftWidthshaftHeight * (shaftsOpen));
}
 
function animate() {
  drawScreen();
  shafts.forEach((shaftshaftNumber) => {
    shaft.workers.forEach((worker) => {
      worker.update(shaftNumber);
    });
  });
  lift.update();
  frame++;
  if (frame > max_frames)
    frame = 0;
  window.requestAnimationFrame(animate);
}
 
function reset() {
  shaftsOpen = 0;
  score = 70;
  lift.y = 0;
  lift.chest = 0;
  lift.loadingCounter = 0;
  shafts.forEach((shaft) => {
    shaft.closed = true;
    shaft.workers = [];
  });
  shafts[0].open();
  shafts[1].open();
  for (let i = shaftsOpeni < max_shaftsi++) {
    shafts[i].closed = true;
    shafts[i].workers = [];
    context.drawImage(closeImg0i * shaftHeight);
  }
  animate();
}
 
function resize() {
  let picSizeX = window.innerWidth;
  let picSizeY = window.innerHeight;
  canvas.style.width = window.innerWidth;
  canvas.style.height = window.innerHeight;
  aspectX = picSizeX / 600;
  aspectY = picSizeY / 600;
}
 
window.onresize = resize;
 
window.onpointerdown = function(event) {
  let x = event.offsetX / aspectX;
  let y = event.offsetY / aspectY;
  let shaftNumber = Math.floor(y / shaftHeight);
  let shaft = shafts[shaftNumber];
  if (x > shaftWidth)
    if (shaftNumber < shaftsOpen && score >= workerCost && shaft.workers.length <= shaft.max_workers) {
      shaft.workers.push(new Worker());
      score = score - workerCost;
    } else {
      if (shaftNumber == shaftsOpen && score >= shaftCost) {
        shaft.open();
      }
    }
};
 
window.onload = function() {
  for (let i = 0i < max_shaftsi++) {
    shafts[i] = new Shaft();
    if (i == 0)
      shafts[i].max_workers = max_workers * 5;
    else
      shafts[i].max_workers = max_workers;
  }
  reset();
  resize();
};
</script>
</body>
</html>


Check out these programming tutorials:

JavaScript:

Tower game (84 lines)

Optical illusion (18)

Spinning squares - visual effect (25)

Oldschool fire effect (20)

Fireworks (60)

Animated fractal (32)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21)

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70)

Sine scroller (30)

Interactive animated sprites

Image transition effect (16)

Your first program in JavaScript: you need 5 minutes and a notepad


Fractals in Excel

Python in Blender 3d:

Domino effect (10 lines)


Wrecking ball effect (14 lines)

3d fractal in Blender Python