SAMEGAME - SOURCE CODE
Emanuele Feronato - emanueleferonato.com

TypeScript + NacatamalOn, built with Vite.


REQUIREMENTS
------------

Node.js 20.19 or newer (or 22.12 or newer). Download it from
https://nodejs.org and install it - npm comes bundled with it, you don't
need to install it separately.

To check what you have, open a terminal and run:

    node -v
    npm -v

Both commands should print a version number.

To play, any recent browser works: the game draws with WebGPU where the
browser has it, and falls back to WebGL2 everywhere else.


GETTING STARTED
---------------

Unzip the project, open a terminal in the project folder, then:

    npm install

This downloads NacatamalOn and the build tools into a node_modules
folder. You only need to do it once.

    npm run dev

This starts the development server and prints its address, usually
http://localhost:5173 - open it in your browser. The page reloads by
itself every time you save a file.

Press Ctrl+C in the terminal to stop the server.


BUILDING FOR RELEASE
--------------------

    npm run build

This checks the types first, then builds. Everything ends up in the dist
folder, ready to upload anywhere: a web server, itch.io, or any static
host. It's plain HTML and JavaScript, so no server-side setup is needed.

    npm run preview

This serves the dist folder locally, to try the build before uploading.

vite.config.ts sets base to './', so the build works from a subfolder
without any change.


PROJECT STRUCTURE
-----------------

    index.html                   page hosting the game canvas
    package.json                 dependencies and the npm scripts
    tsconfig.json                TypeScript configuration
    vite.config.ts               Vite configuration: relative paths
    public/favicon.png           browser tab icon
    src/style.css                page background behind the canvas
    src/main.ts                  game window: size, scaling, scenes
    src/config.ts                game size, colours, dither strength
    src/utils.ts                 functions that paint the tiles and the
                                 sky, and the CRT effect with a custom
                                 colour around the glass
    src/scenes/game.ts           rendering, input, tweens, scoring, levels
    src/vendor/samegame/         the headless engine, with its licence:
        SameGame.ts              the game logic
        FloodFill2D.ts           generic flood fill, used to find regions

There are no image files: the tiles, the sky and the favicon are painted
in code, pixel by pixel. The text uses the font built into NacatamalOn.


HOW IT FITS TOGETHER
--------------------

The engine and the renderer know nothing about each other.

SameGame.ts never touches a canvas. It is generic over the tile type, so
it stores whatever object you hand it and only asks you for two things:
a generator function that produces a new tile, and a comparator that says
whether two tiles match. Call removeRegion(row, col) and it returns a
description of what happened - which tiles were removed, which ones fell,
which ones shifted sideways when a column emptied out.

game.ts takes that description and turns it into tweens: fade the
removed tiles out, drop the fallen ones, bounce the shifted columns into
place, then hand control back to the player. Sprites are pooled rather
than created and destroyed, so nothing is allocated during play.

In NacatamalOn a scene is a plain function that runs once, when the
scene starts. It creates everything up front and asks for what it needs
with hooks: usePointer for clicks, useTween for animations, useTimer for
delays. A tween animates a number rather than an object, and its
onUpdate callback copies the value wherever it belongs: a sprite's x, y
or alpha.

FloodFill2D.ts is the piece underneath: a standalone 4- or 8-directional
flood fill over a 2D grid. It has no idea it is being used for a puzzle
game.


THE 1997 LOOK
-------------

Every frame goes through two screen effects, set up at the top of
game.ts:

- dither cuts each colour channel down to the 8 levels of the Sega Mega
  Drive's 9-bit palette, hiding the steps with an ordered pattern
- the CRT effect draws the result on a slightly curved tube, with
  scanlines and a soft glow

In the game, press C to switch the CRT off and on, and D for the dither.

All the colours of tiles, board and text are snapped to the Mega Drive
palette by megaDrive() in utils.ts, so they stay flat under the dither:
only the sky gradient and the fading tiles turn into the pattern.

main.ts sets pixelRatio to 2, so the CRT draws each scanline as a beam
with a gap rather than just darkening every other row.


THINGS TO TRY
-------------

The constants at the top of game.ts hold the whole board layout:
tileSize, rows, cols, offsetX and offsetY. Change them and the board
reshapes without touching any logic. Keep in mind the game is a fixed
384x216 pixels, set in config.ts, and scaled up by whole multiples to
fit the window: a 16 pixel tile leaves room for about 24 columns and 11
rows. Make the game bigger in config.ts if you want more.

The four tile colours are the four rows of tileShades in utils.ts, each
a light, base and dark shade. Add a fifth row and bump the 4 in the
texture loop and in the generator function, both in game.ts, to make the
game harder.

The comparator is where the interesting variations live. It currently
compares tile types, but nothing stops you from making one type match
everything - a wildcard tile, in about two lines.

The look is a few numbers away. Try COLOR_LEVELS.snes instead of
COLOR_LEVELS.genesis for 32 levels per channel, another CRT preset such
as CRT_PRESETS.pvm or CRT_PRESETS.arcade, or a different bezel colour in
config.ts.

Gravity and mode are arguments to the SameGame constructor: 'up',
'left' or 'right' instead of 'down', 'endless' instead of 'normal'. The
engine handles all of them, but the tweens in game.ts are written for
'down' and 'normal': other directions need makeBlocksFall and
compactTable to move along the right axis, and endless mode needs the
spawned tiles placed on the board.

Best scores are kept in localStorage under the key 'samegame'.


THE ENGINE ON ITS OWN
---------------------

SameGame.ts and FloodFill2D.ts are self-contained: no NacatamalOn
import, no DOM, no dependencies at all. They are the same files,
unchanged, that power the Phaser 4 version of this game: only the scene
was rewritten. Copy the samegame folder into any project and it will work
with a different renderer, on a server, or in a test.

The standalone version, with documentation and more examples, is at
https://github.com/triqui/samegame-engine

The Phaser 4 version of this game is at
https://triqui.itch.io/samegame

The article explaining how the Phaser 4 version was built is at
https://emanueleferonato.com/2026/04/09/phaser-4-html5-samegame-powered-by-samegame-engine-a-headless-typescript-class/

NacatamalOn, with its docs, tutorial and examples, is at
https://nacatamalon.com


LICENCE
-------

Do what you want with the code. A credit is appreciated but not required.

The SameGame engine and NacatamalOn are both MIT licensed. The font,
Nacatamal Arcade, is built into NacatamalOn and derived from Press Start
2P, under the SIL Open Font License 1.1: it can be used freely, also in
commercial games.