# Mahjong Match

A match-3 game played with a complete set of mahjong tiles. Swap adjacent
tiles to form a **chow**, **pong**, or **kong**; matched tiles are claimed
and disappear, the tiles above drop down, and new tiles pour in from the set
above until it runs dry. Clear the board (or run out of moves) as fast as
you can — speed is the score.

## How to run

This game needs no build step. It's plain ES modules and loads
[PixiJS](https://pixijs.com/) v8 directly from a CDN at runtime, so all you
need is a static file server (browsers block ES module imports from a bare
`file://` page):

```
npx serve .
# or: python -m http.server
# or: npm run serve
```

Then open the printed local URL. An internet connection is required the
first time the page loads (to fetch PixiJS from jsdelivr).

> The repo also has a webpack dev/build setup (`npm start` / `npm run
> build`) left over from the project's HTML5 Boilerplate scaffold. It is
> **not** wired up for this game — the game's CDN-based module imports
> aren't something webpack resolves out of the box. Use the static-server
> method above to actually play it.

## The board

The play area is a **5-row by 7-column grid (35 cells)**. At the start of a
level the grid is filled from a shuffled, complete mahjong set; the rest of
the set becomes the **draw pile** that feeds new tiles into the board later.

A complete set here is the standard 136 tiles used for matching purposes:

- **Number suits** — Characters (萬), Bamboo (索), Circles (筒), ranks 1–9,
  4 copies of each rank = 108 tiles.
- **Honor tiles** — 4 Winds (東 南 西 北) and 3 Dragons (中 發, plus a blank
  White Dragon), 4 copies of each = 28 tiles.
- **Total: 34 unique tile types × 4 copies = 136 tiles.**

Flower/season tiles are intentionally left out of the set: there's only one
of each in a real set, so they could never form a chow, pong, or kong and
would just sit on the board forever.

## Controls

Drag a tile onto an orthogonally-adjacent tile (up, down, left, or right) to
swap their positions. The swap always sticks — there's no rule requiring a
move to create a match. If the new arrangement happens to line up a chow,
pong, or kong (for either tile involved, or anywhere else on the board), it's
claimed immediately as described below.

## Matches

A match is 3+ tiles in a straight line (a single row or a single column —
diagonals don't count):

| Match  | Requirement                                                                 | Tiles cleared |
| ------ | ---------------------------------------------------------------------------- | -------------- |
| Chow   | 3 contiguous tiles of the **same number suit** whose ranks form a run of 3 consecutive numbers (e.g. 4-5-6 of Bamboo, in either order) | 3 |
| Pong   | 3 contiguous **identical** tiles (same suit+rank, or the same honor tile)   | 3 |
| Kong   | 4 contiguous **identical** tiles                                            | 4 |

Only number-suit tiles (Characters/Bamboo/Circles) can form a chow — Winds
and Dragons have no rank, so they can only ever pong or kong.

Matches can happen horizontally or vertically. Every match found anywhere on
the board is claimed at once, not just the one touching the tile you moved,
so a single swap can trigger chain reactions as tiles fall and new matches
line up (a cascade).

## Gravity, refills, and the draw pile

When tiles are claimed, everything above them in that column drops down to
fill the gap, and new tiles are dealt from the top of the draw pile to fill
the empty cells at the top. This repeats — including chain reactions caused
by tiles that just fell into place — until the board has no more matches
sitting on it, then control returns to you.

Once the draw pile is empty, cleared cells simply stay empty; the board
shrinks as you keep clearing it out.

## When the level ends

The level ends the moment **a match can never happen again**, given what's
left on the board. Since swapping is unrestricted (any two adjacent tiles
can always trade places, whether or not it matches), any arrangement of the
board's tiles is reachable eventually — so this isn't about whether a match
is one swap away, it's about whether the right tiles still *exist* anywhere
on the board at all: 3+ copies of one tile type (a pong/kong waiting to
happen), or 3 consecutive ranks of the same suit somewhere on the board (a
chow waiting to happen). Once neither is true — whether because the draw
pile ran dry and what's left can't combine, or because the board has been
entirely cleared — the level ends immediately. There is no reshuffle.

## Scoring

Speed is everything: the game tracks the time from your first move until the
level ends. Every tile still sitting on the board when the level ends is
treated as if it cost you 5 extra seconds — so a fully-cleared board is
always worth more than an early stall with tiles left over.

```
finalTime = elapsedSeconds + remainingTiles * 5
score     = max(0, round(100000 - finalTime * 100))
```

(`elapsedSeconds` and the constants live in `js/game/scoring.js` if you want
to retune the formula.)

## Project structure

```
index.html                page shell, HUD, game-over overlay
css/style.css              styling (game-specific rules appended to the H5BP boilerplate)
js/
  app.js                   entry point — boots the GameController
  pixi.js                  re-exports PixiJS v8 from a pinned CDN URL
  GameController.js        Pixi app setup, board rendering, drag input, animation, timer, HUD
  render/
    TileSprite.js           on-board tile: a Container wrapping a face Sprite + highlight Sprite
    tileTextures.js         bakes each tile type's face (Canvas 2D) into a texture up front
  game/
    constants.js            board dimensions and pixel layout
    tileSet.js               tile type definitions + full-set generator/shuffle
    matchLogic.js             chow/pong/kong detection, deadlock (no-move) detection
    Board.js                  grid state: swap, clear, gravity + refill
    scoring.js                score formula
```

See `DEVLOG.md` for implementation notes, the PixiJS bugs worked around
along the way, and how to re-debug things if they resurface.
