A side-scrolling platformer written in plain JavaScript, drawn on a single HTML
canvas. No framework, no bundler, no npm install — three script-less files and
a folder of sprites. Five levels, a flagpole at the end of each.
Locally — the game needs to be served over HTTP (browsers refuse to load the
sprite sheet from a file:// page), and any static server will do:
git clone https://github.com/AdarshGaur/Mario-Platform-Game.git
cd Mario-Platform-Game
python3 -m http.server 8000 # or: npx serve .Then open http://localhost:8000. There is nothing to build.
Deep links — index.html#1-3 starts that level directly, and the URL updates
as you progress, so a refresh drops you back where you were.
| Action | Keys |
|---|---|
| Move | ← → or A D |
| Jump | ↑, W or Space |
| Pause | P or Esc |
| Restart the level (costs a life) | R |
| Mute | M |
On a phone or tablet an on-screen pad appears under the canvas.
| Points | |
|---|---|
| Bumping a coin block from below | 100 |
| Clearing a level | 1000 |
| Each unit left on the clock | 10 |
You get three lives. Falling into a pit or running the clock out costs one and
restarts the level; losing the last one ends the run. Progress (levels unlocked,
best score, mute) is kept in localStorage.
| Name | Width | Coin blocks | What it is | |
|---|---|---|---|---|
| 1-1 | Overworld | 130 tiles | 13 | The original level: pipes, brick rows, a stone staircase. |
| 1-2 | Pipe Valley | 150 tiles | 12 | A forest of pipes with three holes in the ground. |
| 1-3 | Sky Steps | 160 tiles | 8 | Two long chasms crossed on floating stone steps. |
| 1-4 | Brick Fortress | 170 tiles | 15 | Low brick ceilings, stair pyramids, climbing platforms. |
| 1-5 | The Long Run | 200 tiles | 21 | Everything at once, twice as long. |
index.html markup: HUD, canvas, overlay, touch pad
css/style.css everything around the canvas (the game itself is all canvas)
js/levels.js level data as character art, plus the decoder
js/game.js the engine: loop, physics, collision, camera, screens
img/ spritesheet.png (tiles), mario-sprites.png, flag, logo, menu bg
audio/ music and four sound effects
tools/ a level checker and a playtest bot (both optional, Python)
requestAnimationFrame drives step() then render(). step() reads the held
keys, integrates the player, resolves collisions, and checks the two end
conditions (touched the flagpole / fell below the world). render() clamps the
camera, paints the sky as a repeating pattern, dots it with clouds placed at
fixed points in the level, and draws only the tile columns that are actually on
screen — a 200-tile level costs the same per frame as a short one.
A level is 15 rows of text, one character per 32×32 tile:
. sky = stone stair block <> pipe mouth
# ground | flagpole [] pipe body
? coin block F flag
b brick x spent coin block
decodeLevel() in js/levels.js turns the rows into a flat Uint8Array and
records the level's size and where the flagpole is. Everything else — camera
limits, the win position, the length of the level — falls out of the data, so
adding a level is adding an entry to the LEVELS array.
Four numbers define the feel, and they are unchanged from the original single-level version of this game:
GRAVITY = 1 // px/frame², added every frame
FRICTION = 0.9 // both axes, applied after the move
ACCEL = 0.5 // px/frame² while a direction is held
JUMP_IMPULSE = 25 // one-shot upward kickFriction on the horizontal axis caps the run at 4.5 px/frame; the impulse and gravity give an apex of 124 px and a full-speed running jump that covers 160 px. Those three numbers are what every level is designed around — see the envelope below.
The player's collision box is 32×32 sitting at the bottom of the 50px sprite. Collisions are resolved vertically first, then horizontally, and the order is load-bearing: gravity pushes the player a pixel into the floor every frame, so resolving sideways first sees the floor as a wall the box is already touching and refuses to let him walk at all. Landing snaps the feet to the top of the tile below; a head that rises into a block is pushed back out, and if that block was a coin block it becomes a spent block and pays 100 points; only then is the box in the right place to ask what is beside it, which is resolved by undoing the move.
-
Add an entry to
LEVELSinjs/levels.js— 15 rows, all the same width, a|flagpole column with anFbeside its top. -
Stay inside the design envelope. It is not a style guide, it is arithmetic:
- The player climbs at most 3 tiles. A 4-tile wall or pipe is an impassable barrier — it misses by two pixels.
- A 3-tile climb only works if the gap in front of it is ≤ 2 tiles.
- A flat gap can be 4 tiles at most, and only with a tile or two of run-up; 3 is the comfortable maximum.
- A coin block is only bumpable 5 rows above the floor you stand on — and bumping one stops you rising, so never put one over the run-up to a jump you have to make. That costs 158px of reach, leaving 113px.
- Leave 2 empty rows above anything you expect the player to stand on, or he cannot fit there.
-
Check it:
python3 tools/check_levels.py # all levels, about a second python3 tools/check_levels.py 1-3 # just one
The checker is a frame-for-frame copy of the engine's physics run over the level data: from the spawn it searches forward through every reachable player state — position, velocity, feet on the ground or not — choosing each frame between running, running and jumping, and coasting. If nothing ever crosses the flagpole it tells you where the run dries up. It also prints advisory notes about the shapes that have broken this game before (holes wider than four tiles, ceilings low enough to cut a jump short); those are hints, not failures, and can over-report.
1-3 15 rows x 160 cols coins 12 states 9652 finishable OK -
Optionally have a bot play it for real, in a browser:
python3 -m http.server 8877 & python3 tools/playtest.py 1-3It drives headless Chromium over the DevTools protocol, holds right, and jumps at walls and holes. Needs
chromiumandpip install websockets. It is a crude player with no lookahead, so a level it fails is worth a look but is not necessarily broken —check_levels.pyis the authority on whether a route exists at all.
The sprite sheets, font and sounds are Nintendo's, used here for a hobby
learning project. The code is MIT licensed (see LICENSE); the artwork and
audio are not mine to license, so treat this as a study piece rather than
something to ship commercially.
