Click on the green circles to toggle the switches and direct the train to the station shown in the green rectangle.

Railroad switches game


Click here for full screen mode

The point of this game is to direct the train to the correct station by toggling the railroad switches. Each of the switches in the green circles has two available settings (positions) - for example 'up' and 'left'.
Once the train makes it to the station, you get a point and a new destination is selected. How many stations can you visit before the time runs out?

It is a simplified remake of a minigame featured in the 1984 Commodore 64 hit "Donald Duck's Playground". The original is much more playable than my version and looks better, but I'm pretty sure it has literally thousands of lines of machine code:



Interestingly enough, the game was ported a couple of years later to 16-bit platforms, but never to other 8-bit machines (Atari/Spectrum etc.) The full source code is below and here is a brief explanation:

Lines [1-6] set up the HTML5 Canvas.
[7-8] The background image:



(more about this below).
[9-10] The spritesheet used to draw the train and the switches:



[11-12] 'Click' and 'Point' sounds.
[13-25] The railroad grid (map):

I shamelessly ripped the C64 image:

and converted it into a grid:


So the JavaScript grid is an exact replica of the original.
These small images (320x96) took up practically half of C64's screen - it's highest (and only) resolution mode was 320x200. To make it more readable on modern screens, I doubled the size of a grid element from 8x8 to 16x16.

_ = horizontal rail
| = vertical rail
. = empty
D, U, L, R = switches (down, up, left, right)
0-7 = railway stations

The game program doesn't really use the dots or horizontal/vertical rails - theoretically we could replace them with spaces or any other character. But I kept them because the background drawing program (below) does use the rails and the dots made it easier for me to draw the grid.

[26-32] Coordinates and alternative settings of the switches.
[33] Time allotted for one round.
[34] The number of cars (the locomotive counts as a car).
[35] The offset of the train images in the tiles spreadsheet - when drawing the train, the program will skip the four arrow images.
[36] Station names.
[37] Size of every tile (they're all square).
[38] An array of objects - each object is an independent vehicle.
[39] 'grid2' is basically a copy of the 'grid' created only for technical reasons. 'grid' is an array of strings and unfortunately there is no easy way to replace one character in a string. I decided to split every string into substrings of characters. When a switch setting is changed, we update just that one character.
[40] Counter - elapsed time.
Mode - game mode (active play or the 'game over' screen)
targetStation - current destination
points - total points so far

[42-45] The function that changes the canvas dimensions to the window dimensions.
[47-49] Randomly select a new destination.
[51-65] Set initial values of variables when the game starts.
[67-146] Main game loop:
[68] If the game is active (ie. we're not in the 'gameOver' mode):
[69] Draw the background to erase the previous animation from the screen.
[70-88] Draw all the switches (arrows) using the spritesheet. We calculate the offset of the arrow image (left/right/etc.) corresponding to the letter in grid2.
[86] Copy the arrow image from 'tiles.png' to the canvas.
[89-92] Draw the cars on the canvas.
[93-102] Check if the locomotive reached the destination. If so, play a sound, add a point and select a new destination.
[103-124] If a car arrived at a switch, change the direction of the train accordingly.
spriteRow determines if in lines [103-105] the train is drawn pointing up/down etc.
[125-129] If it's a railcar, realign it to the previous one. This prevents them from separating if the player changes the switch between the locomotive and the railcars.
[131-132] The train keeps on moving.
[135-136] The time keeps running out.
[137-139] Update the scoreboard.
[140-143] Switch to gameOver mode if the time ran out.
[145] Get ready for the next animation frame.

[148-170] Handle mouse clicks and touch screen:
[150-151] Since the 'resize' function modified the display dimensions of the canvas, we have to translate the coordinates of the event to our original 608x208 image.
[152-165] Have any of the green buttons been clicked? If so, switch the current value of the button with the alternative one [165-167].
[166-167] If we're in gameOver mode, restart the game.
[173-174] Split grid into grid2 - an array of 1-character strings.
[177-178] Cosmetics.
[179-180] Let's start!


<html>
<body>
<canvas id="myCanvaswidth="608height="192style='position:absoluteleft:0top:0'></canvas>
<script>
const canvas = document.getElementById("myCanvas");
const context = canvas.getContext("2d");
const img = new Image();
img.src = "background.jpg";
const tiles = new Image();
tiles.src = "tiles.png";
const clickSound = new Audio('click.mp3');
const pointSound = new Audio('point.mp3');
const grid = [
'D______________L_______7_____________L', 
'0..............|.....................|', 
'|...D__________D.....................|', 
'|...|..........|.....................|', 
'|...|..........1.....................|', 
'|...D__6_______R___________R_________U', 
'|...|......................4.........|', 
'|...|......................|.........|', 
'|...D__________R___________U.........|', 
'|...|..........|...........|.........|', 
'|...2..........3...........|.........5', 
'R___R__________R___________R_________U'];
const buttons = [
  {  x15,  y0,  value: "D"},
  {  x15,  y2,  value: "L"},
  {  x4,  y5,  value: "R"},
  {  x4,  y8,  value: "R"},
  {  x15,  y8,  value: "D"},
  {  x27,  y11,  value: "U"}];
const roundTime = 500;
const carCount = 3;
const trainSpriteOffset = 4;
const stations = ['Ducktropolis', 'Ducks Landing', 'Duck City', 'Duckville', 'Duckburg', 'Ducktown', 'Duck Valley', 'Duck Corners'];
const size = 16;
let cars = [];
let grid2 = [];
let countermodetargetStationpoints;
 
function resize() {
  canvas.style.width = window.innerWidth;
  canvas.style.height = window.innerHeight;
}
 
function newTarget() {
  targetStation = Math.floor(Math.random() * stations.length);
}
 
function reset() {
  mode = 'play';
  counter = 0;
  for (let n = 0n < carCountn++) {
    cars[n] = {
      x: (20 + n) * size,
      y0,
      xSpeed: -1,
      ySpeed0,
      spriteRow0
    };
  }
  points = 0;
  newTarget();
}
 
function animate() {
  if (mode == 'play') {
    context.drawImage(img00);
    buttons.forEach((button)=>{
      let offset;
      switch (grid2[button.y][button.x]) {
      case 'U':
        offset = 0;
        break;
      case 'D':
        offset = 1;
        break;
      case 'R':
        offset = 2;
        break;
      case 'L':
        offset = 3;
        break;
      }
      context.drawImage(tilesoffset * size0sizesizebutton.x * sizebutton.y * sizesizesize);
    }
    );
    cars.forEach((car,index)=>{
      context.drawImage(tiles, (index + trainSpriteOffset) * sizecar.spriteRow * sizesizesizecar.xcar.ysizesize);
    }
    );
    cars.forEach((car,index)=>{
      let gridX = car.x / size;
      let gridY = car.y / size;
      if (gridX == Math.floor(gridX) && gridY == Math.floor(gridY)) {
        let tileValue = grid2[gridY][gridX];
        if (index == 0 && tileValue == targetStation) {
          pointSound.play();
          points++;
          newTarget();
        }
        switch (tileValue) {
        case 'L':
          car.xSpeed = -1;
          car.ySpeed = 0;
          car.spriteRow = 0;
          break;
        case 'R':
          car.xSpeed = 1;
          car.ySpeed = 0;
          car.spriteRow = 1;
          break;
        case 'U':
          car.xSpeed = 0;
          car.ySpeed = -1;
          car.spriteRow = 2;
          break;
        case 'D':
          car.xSpeed = 0;
          car.ySpeed = 1;
          car.spriteRow = 3;
          break;
        }
        if (index > 0 && tileValue > 'A' && tileValue < 'Z') {
          car.xSpeed = cars[index - 1].xSpeed;
          car.ySpeed = cars[index - 1].ySpeed;
          car.spriteRow = cars[0].spriteRow;
        }
      }
      car.x += car.xSpeed;
      car.y += car.ySpeed;
    }
    );
    counter++;
    let timeLeft = Math.floor(roundTime - counter / 10);
    context.fillText('Time left: ' + timeLeft290125);
    context.fillText(points46070);
    context.fillText(stations[targetStation], 46050);
    if (timeLeft == 0) {
      mode = 'gameOver';
      context.fillText('GAME OVERClick to restart.', 230110);
    }
  }
  window.requestAnimationFrame(animate);
}
 
window.onpointerdown = function(event) {
  if (mode == 'play') {
    let x1 = Math.floor(event.offsetX / (window.innerWidth / (grid[0].length * size)));
    let y1 = Math.floor(event.offsetY / (window.innerHeight / (grid.length * size)));
    buttons.forEach((button)=>{
      let x2 = button.x * size + size / 2;
      let y2 = button.y * size + size / 2;
      let distance = Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2));
      if (distance <= size) {
        let x = button.x;
        let y = button.y;
        let temp = grid2[y][x];
        grid2[y][x] = button.value;
        button.value = temp;
        clickSound.play();
      }
    }
    );
  } else
    reset();
  event.preventDefault();
}
;
 
window.onresize = resize;
grid.forEach((r,index)=>{
  grid2[index] = Array.from(r);
}
);
context.fillStyle = 'green';
context.font = '14px sans-serif';
resize();
reset();
window.onload = animate();
</script>
</body>
</html>


Below is a small code snippet that generates the background image.
It has its own tile set:



Theoretically this code could become part of the game and be executed in every animation frame (instead of a one-time import in lines [5-6]), but that wouldn't be very efficient.
Please also note that this image can not be transparent in the game (otherwise the train would leave an ugly trail). If you just run the program below and right-click to save the image, it will save as a PNG with transparent background.
To eliminate the transparency, I just saved it as JPG.

<html>
<body>
<canvas id="myCanvaswidth="608height="208"></canvas>
<script>
let tiles = new Image();
tiles.src = "map_tiles.png";
 
window.onload = function() {
  let canvas = document.getElementById("myCanvas");
  let context = canvas.getContext("2d");
  let grid = [
  'D______________L_______7_____________L', 
  '0..............|.....................|', 
  '|..............|.....................|',   
  '|...D__________D.....................|', 
  '|...|..........|.....................|', 
  '|...|..........1.....................|', 
  '|...D__6_______R___________R_________U', 
  '|...|......................4.........|', 
  '|...|......................|.........|', 
  '|...D__________R___________U.........|', 
  '|...|..........|...........|.........|', 
  '|...2..........3...........|.........5', 
  'R___R__________R___________R_________U'];
  const buttons = [{
    x15,
    y0,
    value: "D",
  }, {
    x15,
    y3,
    value: "L"
  }, {
    x4,
    y6,
    value: "R"
  }, {
    x4,
    y9,
    value: "R"
  }, {
    x15,
    y9,
    value: "D"
  }, {
    x27,
    y12,
    value: "U"
  }];
  const stations = ['Ducktropolis', 'Ducks Landing', 'Duck City', 'Duckville', 'Duckburg', 'Ducktown', 'Duck Valley', 'Duck Corners'];
  const max_rows = grid.length
    , max_columns = grid[0].length
    , size = 16;
  context.fillStyle = 'lightgreen';
  context.font = '14px sans-serif';
  buttons.forEach((button)=>{
    context.beginPath();
    context.arc(button.x * size + size / 2button.y * size + size / 2size02 * Math.PI);
    context.fill();
  }
  );
  context.fillRect(4504512020);
  context.fillStyle = 'black';
  for (let row = 0row < max_rowsrow++)
    for (let column = 0column < max_columnscolumn++) {
      let offset;
      let tileValue = grid[row][column];
      if (tileValue >= '0' && tileValue <= '9') {
        offset = 2;
        let xShift = 0;
        let yShift = 0;
        let stationName = stations[tileValue];
        if (stationName == 'Ducktown')
          xShift = -5;
        if (stationName == 'Ducktropolis')
          yShift = -.3;
        if (stationName == 'Duck Corners' || stationName == 'Duck Valley') {
          yShift = .8;
          xShift = -1;
        }
        context.fillText(stationName, (column + 1 + xShift) * size, (row + 1 + yShift) * size);
      }
      switch (tileValue) {
      case '_':
        offset = 0;
        break;
      case '|':
        offset = 1;
        break;
      case 'U':
        offset = 3;
        break;
      case 'D':
        offset = 4;
        break;
      case 'R':
        offset = 5;
        break;
      case 'L':
        offset = 6;
        break;
      }
      context.drawImage(tilesoffset * size0sizesizecolumn * sizerow * sizesizesize);
    }
  context.fillText('GO TO:', 40060);
  context.fillText('Points:', 40080);
}
;
</script>
</body>
</html>



This section in gray font is just a small rant about the buttons. Feel free to ignore it.
Initially I started out with two grids - one for the stations and switches (this grid remains unchanged in the current version of the game) and another one for alternative settings for the switches.
The player could only toggle the switch by clicking the exact box in the grid. The program was shorter (and I think more elegant), because it just converted the click coordinates to grid coordinates and checked if the second grid has a value in the given box. There was no need to calculate any distance.
However I quickly noticed that this solution does not work well on mobile devices - it is difficult to click exactly the right box. So instead of the second array of characters, I introduced an array of objects ('buttons'), and the program now calculates the distance between the click and each button. This allows us to 'accept' clicks which are close, but not necessarily exactly on the grid element with the switch. The 'acceptance' area is represented by the green circles.


Check out these programming tutorials:

JavaScript:

Tower game (84 lines)

Optical illusion (18 lines)

Spinning squares - visual effect (25 lines)

Oldschool fire effect (20 lines)

Fireworks (60 lines)

Animated fractal (32 lines)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21 lines)

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

Tile map editor (70 lines)

Sine scroller (30 lines)

Interactive animated sprites

Image transition effect (16 lines)

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






Donald Duck is a trademark of Disney Enterprises, Inc.