-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMahjong.html
More file actions
318 lines (312 loc) · 20 KB
/
Mahjong.html
File metadata and controls
318 lines (312 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="#0c1016">
<title>Mahjong Solitaire — Free Online Tile Match Game | Board Gaming Hub</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Play Mahjong Solitaire free online: classic Chinese tile-matching game. Match free pairs to clear the layout. Single-page browser game, no signup, no downloads.">
<meta name="keywords" content="mahjong, mahjong solitaire, free mahjong online, tile matching game, classic mahjong">
<link rel="canonical" href="https://boardgaminghub.com/Mahjong.html">
<meta property="og:type" content="website">
<meta property="og:url" content="https://boardgaminghub.com/Mahjong.html">
<meta property="og:title" content="Mahjong Solitaire — Free Online">
<meta property="og:description" content="Match identical free tiles to clear the layout. Free, in-browser, single-file HTML.">
<meta name="twitter:card" content="summary">
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "VideoGame", "name": "Mahjong Solitaire",
"description": "Match identical pairs of free tiles to clear a stacked layout.",
"url": "https://boardgaminghub.com/Mahjong.html", "genre": "Tile Matching",
"applicationCategory": "Game", "operatingSystem": "Any",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" } }
</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Board Gaming Hub","item":"https://boardgaminghub.com/"},{"@type":"ListItem","position":2,"name":"Puzzles & Classics","item":"https://boardgaminghub.com/#puzzles"},{"@type":"ListItem","position":3,"name":"Mahjong Solitaire","item":"https://boardgaminghub.com/Mahjong.html"}]}</script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How do you play Mahjong Solitaire?","acceptedAnswer":{"@type":"Answer","text":"Tap two matching free tiles to remove them as a pair. A tile is free if no tile sits on top of it AND its left or right side is unblocked. Win by removing every tile from the layout. Mahjong Solitaire uses the same 144-tile set as the four-player tile game but is a different game — it is single-player matching, not the betting/scoring game."}},{"@type":"Question","name":"What does it mean for a tile to be free?","acceptedAnswer":{"@type":"Answer","text":"A tile is free (selectable) when (1) no tile sits on top of it AND (2) at least one of its left or right edges is open (no tile directly beside it on that side). A tile blocked by neighbors on both sides cannot be matched until one of those neighbors is removed."}},{"@type":"Question","name":"What tile types are in the set?","acceptedAnswer":{"@type":"Answer","text":"The 144-tile set: 108 suit tiles (Bamboo, Characters, Dots — each 1–9 in 4 copies = 36 per suit), 16 Wind honors (East/South/West/North × 4), 12 Dragon honors (Red/Green/White × 4), and 8 bonus tiles (4 Flowers + 4 Seasons). Suits and honors must match the exact tile to pair. Flowers match any other Flower; Seasons match any other Season — so all 4 Flowers and all 4 Seasons are mutually pairable."}},{"@type":"Question","name":"Are all Mahjong Solitaire layouts solvable?","acceptedAnswer":{"@type":"Answer","text":"Layouts vary. Many implementations (and this one) generate boards that are guaranteed solvable; others shuffle randomly and may produce dead-ends. Even on a solvable board, you can lock yourself out by removing pairs in the wrong order. The classic Turtle layout is solvable about 75% of the time with random pairings; near 100% with foresight."}},{"@type":"Question","name":"How is Mahjong Solitaire different from Mahjong (the four-player game)?","acceptedAnswer":{"@type":"Answer","text":"Different games sharing the same tile set. Four-player Mahjong is a turn-based draw-and-discard betting game similar to Rummy, scored by hand value. Mahjong Solitaire is a single-player tile-matching puzzle, invented in the late 1970s for early personal computers. They share tiles but no rules."}}]}</script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2835365593874464" crossorigin="anonymous"></script>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0;
background:
radial-gradient(ellipse at top, #1a1208 0%, #0a0604 70%) fixed,
#0a0604;
color: #e8d8b8; font-family: Georgia, "Times New Roman", serif; }
#wrap { max-width: 900px; margin: 0 auto; padding: 28px 12px 60px; text-align: center; }
h1 { font-size: 2.2em; letter-spacing: 10px; margin: 0 0 6px;
background: linear-gradient(180deg, #ffe8a8 0%, #d4a050 50%, #905028 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
text-shadow: 0 2px 6px rgba(0,0,0,0.6); }
.tag { color: #a09078; letter-spacing: 4px; font-size: 0.82em; margin-bottom: 16px; font-style: italic; }
.controls { margin: 12px 0; display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
button { background: linear-gradient(180deg, #3a2818, #1f1408); color: #f0d89c; border: 1px solid #5a4028; border-radius: 4px; padding: 8px 14px; font-family: inherit; letter-spacing: 2px; font-size: 0.82em; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,220,150,0.2), 0 2px 4px rgba(0,0,0,0.4); }
button:hover { background: linear-gradient(180deg, #4a3624, #2a1c10); border-color: #7a5838; }
.stats { color: #a09078; font-size: 0.85em; letter-spacing: 3px; margin: 8px 0 16px; }
.stats span { color: #ffd896; text-shadow: 0 1px 0 #000; }
#boardWrap { overflow: auto; padding: 16px 0;
background:
radial-gradient(ellipse at center, rgba(80,40,20,0.3) 0%, transparent 70%);
border-radius: 8px; }
#board { position: relative; margin: 0 auto; }
/* Tile body: aged ivory with mother-of-pearl iridescent shimmer */
.tile { position: absolute; width: 50px; height: 64px;
background:
radial-gradient(ellipse at 25% 20%, rgba(255,220,255,0.35), transparent 55%),
radial-gradient(ellipse at 75% 70%, rgba(180,255,235,0.28), transparent 55%),
radial-gradient(ellipse at 80% 25%, rgba(255,240,200,0.25), transparent 60%),
radial-gradient(ellipse at 20% 80%, rgba(255,200,220,0.2), transparent 55%),
linear-gradient(160deg, #fdf6e8 0%, #f4e8d4 30%, #fcefe2 55%, #f0e4d0 80%, #e8d8c0 100%);
border: 1px solid #b8a078; border-radius: 6px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.6),
inset 0 -1px 0 rgba(140,100,60,0.2),
2px 2px 0 #2a1810, 3px 3px 0 #5a3a20,
4px 6px 12px rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
font-size: 30px; cursor: pointer; user-select: none;
transition: transform 0.12s, box-shadow 0.12s; line-height: 1; }
/* Bamboo: jade inlay */
.tile.bam {
color: transparent;
background:
radial-gradient(ellipse at 25% 20%, rgba(255,220,255,0.3), transparent 55%),
radial-gradient(ellipse at 75% 70%, rgba(180,255,235,0.25), transparent 55%),
linear-gradient(160deg, #fdf6e8 0%, #f4e8d4 30%, #fcefe2 60%, #e8d8c0 100%);
text-shadow: none;
}
.tile.bam::after { content: ''; }
.tile.bam { background-clip: padding-box; }
/* Use a wrapping technique via child not possible; instead apply gradient text-fill */
.tile.bam { color: #1a6048;
text-shadow: 0 1px 0 rgba(255,255,255,0.6), 0 0 6px rgba(60,180,120,0.3); }
/* Characters: ruby/cinnabar inlay */
.tile.chr { color: #9a1820;
text-shadow: 0 1px 0 rgba(255,255,255,0.5), 0 0 5px rgba(200,40,40,0.3); }
/* Locked tiles dim */
.tile.locked { filter: brightness(0.6) saturate(0.7); cursor: default; }
/* Selected: gilded glow */
.tile.sel {
background:
radial-gradient(ellipse at 25% 20%, rgba(255,255,200,0.55), transparent 55%),
radial-gradient(ellipse at 75% 70%, rgba(255,220,140,0.4), transparent 55%),
linear-gradient(160deg, #fff8d0 0%, #f8e088 40%, #d4a040 100%);
border-color: #b07810;
transform: translate(-2px, -2px);
box-shadow:
inset 0 1px 0 rgba(255,255,200,0.8),
0 0 0 2px rgba(255,200,80,0.5),
4px 4px 0 #2a1810, 5px 5px 0 #5a3a20,
6px 8px 16px rgba(255,180,40,0.4); }
.tile.hint { animation: pulse 0.8s ease-in-out 2; }
@keyframes pulse { 0%,100%{transform:translate(0,0);} 50%{transform:translate(-3px,-3px); filter: brightness(1.2);} }
#msg { margin-top: 14px; min-height: 1.4em; color: #f0d89c; letter-spacing: 3px; font-size: 0.95em; }
nav { margin-top: 22px; font-size: 0.82em; letter-spacing: 2px; }
nav a { color: #a09078; text-decoration: none; margin: 0 10px; }
nav a:hover { color: #f0d89c; }
.info { color: #8a7858; font-size: 0.82em; line-height: 1.6; margin-top: 30px; text-align: left; padding: 0 6px; }
.info h2 { color: #d4a050; font-size: 0.95em; letter-spacing: 4px; text-transform: uppercase; margin: 18px 0 8px; }
.info strong { color: #f0d89c; }
</style>
</head>
<body>
<div id="wrap">
<h1>MAHJONG</h1>
<div class="tag">SOLITAIRE TILE MATCHING</div>
<div class="controls">
<button id="newgame">NEW</button>
<button id="hint">HINT</button>
<button id="undo">UNDO</button>
<button id="shuffle">SHUFFLE</button>
</div>
<div class="stats">PAIRS LEFT <span id="left">0</span> · TIME <span id="time">0:00</span></div>
<div id="boardWrap"><div id="board"></div></div>
<div id="msg"></div>
<nav><a href="index.html">All games</a> · <a href="2048.html">2048</a> · <a href="Wordform.html">Wordform</a></nav>
<div class="info">
<h2>How to play Mahjong Solitaire</h2>
<p>Match pairs of identical <strong>free</strong> tiles to remove them. A tile is free when nothing rests on top of it and either its left or right side is unblocked. Clear all 72 tiles to win.</p>
<h2>Strategy</h2>
<p>Plan ahead — bad early matches can lock the board. When you see four of a kind exposed, match all four; otherwise, match pairs that unblock the most tiles. Use <strong>HINT</strong> when stuck, or <strong>SHUFFLE</strong> if no moves remain.</p>
</div>
</div>
<script>
(() => {
const $ = id => document.getElementById(id);
const TILE_W = 50, TILE_H = 64, OFF_X = 6, OFF_Y = 8;
// Faces: 9 bamboo + 9 characters = 18, ×4 = 72 tiles
const FACES = [
{ch:'🀐',cls:'bam'},{ch:'🀑',cls:'bam'},{ch:'🀒',cls:'bam'},{ch:'🀓',cls:'bam'},{ch:'🀔',cls:'bam'},{ch:'🀕',cls:'bam'},{ch:'🀖',cls:'bam'},{ch:'🀗',cls:'bam'},{ch:'🀘',cls:'bam'},
{ch:'🀇',cls:'chr'},{ch:'🀈',cls:'chr'},{ch:'🀉',cls:'chr'},{ch:'🀊',cls:'chr'},{ch:'🀋',cls:'chr'},{ch:'🀌',cls:'chr'},{ch:'🀍',cls:'chr'},{ch:'🀎',cls:'chr'},{ch:'🀏',cls:'chr'}
];
// Layout: nested layers
function buildLayout() {
const tiles = [];
const rects = [
{z:0, c0:0, c1:8, r0:0, r1:4}, // 32
{z:1, c0:1, c1:7, r0:0, r1:4}, // 24
{z:2, c0:2, c1:6, r0:0, r1:3}, // 12
{z:3, c0:3, c1:5, r0:0, r1:2} // 4 — total 72
];
for (const r of rects) for (let row=r.r0; row<r.r1; row++) for (let col=r.c0; col<r.c1; col++)
tiles.push({z:r.z, row, col, alive:true, face:null, id: tiles.length});
return tiles;
}
let tiles, sel, history, t0, timer;
function shuffleFaces(tiles) {
const deck = [];
for (const f of FACES) for (let i=0;i<4;i++) deck.push(f);
for (let i=deck.length-1;i>0;i--) { const j = Math.floor(Math.random()*(i+1)); [deck[i],deck[j]]=[deck[j],deck[i]]; }
tiles.forEach((t,i) => t.face = deck[i]);
}
function isCovered(t) {
return tiles.some(x => x.alive && x.z === t.z + 1 && x.row === t.row && x.col === t.col);
}
function isSideBlocked(t) {
const left = tiles.some(x => x.alive && x.z === t.z && x.row === t.row && x.col === t.col - 1);
const right = tiles.some(x => x.alive && x.z === t.z && x.row === t.row && x.col === t.col + 1);
return left && right;
}
function isFree(t) { return t.alive && !isCovered(t) && !isSideBlocked(t); }
function findMove() {
const free = tiles.filter(isFree);
for (let i=0;i<free.length;i++) for (let j=i+1;j<free.length;j++)
if (free[i].face.ch === free[j].face.ch) return [free[i].id, free[j].id];
return null;
}
function newGame() {
tiles = buildLayout(); sel = null; history = [];
do { shuffleFaces(tiles); } while (!findMove());
if (timer) clearInterval(timer); t0 = Date.now();
timer = setInterval(() => {
const s = Math.floor((Date.now()-t0)/1000);
$('time').textContent = Math.floor(s/60)+':'+String(s%60).padStart(2,'0');
}, 250);
$('msg').textContent = ''; render();
}
function render() {
const b = $('board');
let maxC = 0, maxR = 0, maxZ = 0;
tiles.forEach(t => { if (t.col>maxC) maxC=t.col; if (t.row>maxR) maxR=t.row; if (t.z>maxZ) maxZ=t.z; });
b.style.width = ((maxC+1)*TILE_W + maxZ*OFF_X + 20) + 'px';
b.style.height = ((maxR+1)*TILE_H + maxZ*OFF_Y + 20) + 'px';
b.innerHTML = '';
const sorted = tiles.slice().sort((a,b) => a.z - b.z || a.row - b.row || a.col - b.col);
for (const t of sorted) {
if (!t.alive) continue;
const d = document.createElement('div');
d.className = 'tile ' + t.face.cls;
if (!isFree(t)) d.classList.add('locked');
if (sel === t.id) d.classList.add('sel');
d.style.left = (t.col*TILE_W - t.z*OFF_X) + 'px';
d.style.top = (t.row*TILE_H - t.z*OFF_Y) + 'px';
d.style.zIndex = t.z*100 + t.row*10 + t.col;
d.textContent = t.face.ch;
d.onclick = () => click(t.id);
b.appendChild(d);
}
const live = tiles.filter(x => x.alive).length;
$('left').textContent = Math.floor(live/2);
if (live === 0) { clearInterval(timer); $('msg').textContent = 'CLEARED IN ' + $('time').textContent; }
else if (!findMove()) $('msg').textContent = 'NO MOVES — SHUFFLE OR UNDO';
}
function click(id) {
const t = tiles.find(x => x.id === id);
if (!t || !t.alive || !isFree(t)) return;
if (sel === id) { sel = null; render(); return; }
if (sel === null) { sel = id; render(); return; }
const s = tiles.find(x => x.id === sel);
if (s.face.ch === t.face.ch) {
history.push([sel, id]);
s.alive = false; t.alive = false; sel = null;
} else {
sel = id;
}
render();
}
function undo() {
if (!history.length) return;
const [a,b] = history.pop();
tiles.find(x=>x.id===a).alive = true;
tiles.find(x=>x.id===b).alive = true;
sel = null; $('msg').textContent = ''; render();
}
function hint() {
const m = findMove(); if (!m) { $('msg').textContent = 'NO MATCHES'; return; }
render();
const live = tiles.filter(x=>x.alive).sort((a,b) => a.z - b.z || a.row - b.row || a.col - b.col);
document.querySelectorAll('.tile').forEach((el, i) => {
const t = live[i]; if (!t) return;
if (m.includes(t.id)) el.classList.add('hint');
});
}
function shuffleAlive() {
const alive = tiles.filter(x => x.alive);
const faces = alive.map(t => t.face);
for (let attempt = 0; attempt < 50; attempt++) {
for (let i=faces.length-1;i>0;i--) { const j = Math.floor(Math.random()*(i+1)); [faces[i],faces[j]]=[faces[j],faces[i]]; }
alive.forEach((t,i) => t.face = faces[i]);
if (findMove()) break;
}
sel = null; $('msg').textContent = findMove() ? '' : 'NO MOVES POSSIBLE — UNDO'; render();
}
$('newgame').onclick = newGame;
$('undo').onclick = undo;
$('hint').onclick = hint;
$('shuffle').onclick = shuffleAlive;
newGame();
})();
</script>
<script src="/analytics.js"></script>
<section class="seo-content">
<h2>About Mahjong Solitaire</h2>
<p>Mahjong Solitaire is a single-player tile-matching puzzle using the 144-tile mahjong set. Match pairs of identical (or matching-category) free tiles to remove them from a stacked layout. Despite the shared name and tile set, this is a different game from <em>full</em> Mahjong (the four-player draw-and-discard game). Mahjong Solitaire was invented in 1981 for early personal computers and popularized as <em>Shanghai</em> in 1986.</p>
<h3>How to play</h3>
<p>Tap two matching free tiles to remove them as a pair. A tile is <strong>free</strong> when (1) no tile sits on top of it AND (2) at least one of its left or right edges has no tile directly beside it. Win by clearing every tile from the layout. You can lock yourself out by removing tiles in the wrong order even on a solvable board, so plan ahead — tiles buried deeper require their cover removed first.</p>
<h3>Tile set: 144 tiles</h3>
<table>
<thead><tr><th>Category</th><th>Tiles</th><th class="num">Count</th><th>Match rule</th></tr></thead>
<tbody>
<tr><td>Bamboo (1–9)</td><td>One suit, 4 of each</td><td class="num">36</td><td>Exact tile</td></tr>
<tr><td>Characters (1–9)</td><td>One suit, 4 of each</td><td class="num">36</td><td>Exact tile</td></tr>
<tr><td>Dots / Circles (1–9)</td><td>One suit, 4 of each</td><td class="num">36</td><td>Exact tile</td></tr>
<tr><td>Winds (E, S, W, N)</td><td>Honor — 4 of each</td><td class="num">16</td><td>Exact tile</td></tr>
<tr><td>Dragons (Red, Green, White)</td><td>Honor — 4 of each</td><td class="num">12</td><td>Exact tile</td></tr>
<tr><td>Flowers (4 unique)</td><td>Bonus — 1 each</td><td class="num">4</td><td>Any flower matches</td></tr>
<tr><td>Seasons (4 unique)</td><td>Bonus — 1 each</td><td class="num">4</td><td>Any season matches</td></tr>
<tr><td><strong>Total</strong></td><td>—</td><td class="num"><strong>144</strong></td><td>—</td></tr>
</tbody>
</table>
<h3>Frequently asked questions</h3>
<details><summary>How do you play?</summary><p>Tap two matching free tiles to remove the pair. Repeat until the board is empty. A tile is free when nothing is on top of it AND its left or right side is open.</p></details>
<details><summary>What does "free" mean?</summary><p>(1) No tile sits on top of it. (2) At least one of its left or right edges has no neighbor. A tile blocked on both sides is not selectable until a neighbor is removed.</p></details>
<details><summary>What tile types are there?</summary><p>108 suit tiles (Bamboo, Characters, Dots — each 1–9 in 4 copies), 16 Winds, 12 Dragons, 4 Flowers, 4 Seasons. Suits and honors require exact match; Flowers and Seasons match any tile of the same category.</p></details>
<details><summary>Are all layouts solvable?</summary><p>Many implementations (this one included) generate guaranteed-solvable boards. Even on a solvable board, removing pairs in the wrong order can lock you out — foresight matters.</p></details>
<details><summary>How is this different from full Mahjong?</summary><p>Full Mahjong is a four-player turn-based draw-and-discard game similar to Rummy, scored by hand value. Mahjong Solitaire is a single-player tile-matching puzzle. They share the tile set; nothing else.</p></details>
<h3>Related games</h3>
<ul>
<li><a href="Solitaire.html">Solitaire</a> — solo card game, similar pace.</li>
<li><a href="Sudoku.html">Sudoku</a> — pure-logic puzzle.</li>
<li><a href="Minesweeper.html">Minesweeper</a> — deduction puzzle.</li>
<li><a href="2048.html">2048</a> — solo number puzzle.</li>
</ul>
</section>
<section style="max-width:900px;margin:60px auto 40px;padding:0 20px;font-family:Georgia,'Times New Roman',serif;color:#d8d0c0;">
<h2 style="color:#f0d89c;letter-spacing:4px;font-size:1.0em;text-transform:uppercase;border-bottom:1px solid #2a3540;padding-bottom:8px;margin-bottom:16px;">Discussion</h2>
<p style="color:#8098a8;font-size:0.9em;margin-bottom:18px;line-height:1.5;">Sign in with GitHub to share strategies, ask questions, or report a bug.</p>
<div class="giscus"></div>
</section>
<script src="https://giscus.app/client.js"
data-repo="mf4633/board-gaming"
data-repo-id="R_kgDOKyyThA"
data-category="Announcements"
data-category-id="DIC_kwDOKyyThM4C75Cz"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="dark"
data-lang="en"
crossorigin="anonymous"
async></script>
<script src="/nav.js" defer></script>
</body>
</html>