136
遊び方・ゲームルール
四川省とは麻雀牌を使ったパズルゲーム
2つずつ牌を取り除く。牌を取り除けるのは以下の条件のときである。

同種の2つの牌が隣接しているとき
同種の2つの牌を他の牌のない場所を通る水平・鉛直の直線で結ぶ際、その線が曲がる回数が2回以内のとき。
これを繰り返して牌を取り除いていく。すべての牌を取り除くと勝ちとなる。
牌を取り除く順によっては終了できず手詰まりとなることもある。
麻雀を知らない人でもとっつきやすいよう表記を英数字や記号にしています。
動画でみる
上海
view source
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 //アルゴリズムと記法の勉強
document.title =
'四川省 二角ルートでつながるペアの牌を消していくゲーム'
;
$(
function
(){
$(
'#demo'
).after($(
'#rule'
));
});
const seOK =
new
Audio(
'/mp3/ok.mp3'
);
const seMiss =
new
Audio(
'/mp3/miss.mp3'
);
const seLose =
new
Audio(
'/mp3/lose.mp3'
);
const seClear =
new
Audio(
'/mp3/clear.mp3'
);
const { min, max, floor, ceil, random } = Math;
const pick = a => a.splice(a.length * random(), 1)[0];
const range = (start, stop, step) => {
if
(stop === undefined) {
stop = start;
start = 0;
}
step = step || (stop < start ? -1 : 1);
const length = max(0, ceil((stop - start) / step));
return
Array.from({ length }, (_, i) => start + step * i);
};
const N = 17 * 8;
const W = 17 + 4;
const H = 8 + 4;
const X = p => p % W;
const Y = p => floor(p / W);
const fromXY = (x, y) => x + y * W;
const fromYX = (y, x) => fromXY(x, y);
const create = () => {
const tiles = range(N).map(i => 1 + floor(i / 4));
const board = range(W * H).map(p => {
const d = min(X(p), Y(p), W - 1 - X(p), H - 1 - Y(p));
return
d === 0 ? -1 : d === 1 ? 0 : pick(tiles);
});
return
{ board, target: -1, rest: N };
};
const update = (state, p) => {
const { board, target, rest } = state;
if
(board[p] <= 0) {
return
state;}
if
(target < 0)
return
{ ...state, target: p };
if
(!test(board, target, p)) {seMiss.play();
return
{ ...state, target: -1 };}
seOK.currentTime = 0;
seOK.play();
return
{
board: board.map((v, i) => i === p || i === target ? 0 : v),
target: -1,
rest: rest - 2
};
};
const move = (board, p, d) => board[p + d] ? p : move(board, p + d, d);
const pass = (board, p, q, U, V, C) => {
const e = C(1, 0);
const u0 = max(U(move(board, p, -e)), U(move(board, q, -e)));
const u1 = min(U(move(board, p, +e)), U(move(board, q, +e)));
const v0 = min(V(p), V(q)) + 1;
const v1 = max(V(p), V(q)) - 1;
const us = range(u0, u1 + 1, 1);
const vs = range(v0, v1 + 1, 1);
return
us.some(u => vs.every(v => board[C(u, v)] === 0));
};
const test = (board, p, q) =>
p !== q && board[p] === board[q] && (
pass(board, p, q, X, Y, fromXY) ||
pass(board, p, q, Y, X, fromYX)
);
const solve = state => {
while
(state.rest) {
const pair = findPair(state.board);
if
(!pair)
return
false
;
state = update(state, pair[0]);
state = update(state, pair[1]);
}
return
true
;
};
const findPair = board => {
const pairs = {};
for
(const [p, v] of board.entries()) {
if
(v <= 0) {
continue
;
}
if
(!pairs[v]) {
pairs[v] = [p];
continue
;
}
for
(const q of pairs[v]) {
if
(test(board, p, q)) {
return
[p, q];
}
}
pairs[v].push(p);
}
};
/*
const tileChar = v =>
v < 1 ? '' :
v < 8 ? '東南西北中發 '[v - 1] :
v < 17 ? '一二三四五六七八九'[v - 8] :
//v < 26 ? String.fromCharCode(0x2160 + v - 17) :
v < 26 ? '123456789'[v - 17] :
'ABCDEFGHI'[v - 26];
//String.fromCharCode(0x2460 + v - 26);
*/
const tileChar = v =>
v < 1 ?
''
:
v < 8 ?
'●○★◆◇■□'
[v - 1] :
v < 17 ?
'abcdefxyz'
[v - 8] :
v < 26 ?
'123456789'
[v - 17] :
'ABCDEFXYZ'
[v - 26];
const tileColor = v =>
v < 1 ?
'c-x'
:
v < 8 ?
'c-r'
:
v < 17 ?
'c-b'
:
v < 26 ?
'c-g'
:
'c-y'
;
const main = async () => {
const demo = document.getElementById(
'demo'
);
const message = document.createElement(
'p'
);
demo.appendChild(message);
message.innerHTML =
'generate...'
;
let state = create();
while
(!solve(state)) {
await
new
Promise(f => requestAnimationFrame(f));
state = create();
}
const view = document.createElement(
'div'
);
view.classList.add(
'board'
);
demo.appendChild(view);
const cells = range(H).flatMap(y => {
const row = document.createElement(
'div'
);
view.appendChild(row);
return
range(W).map(x => {
const cell = document.createElement(
'a'
);
row.appendChild(cell);
cell.onclick = _ => render(state = update(state, fromXY(x, y)));
return
cell;
});
});
let prevRest = 0;
const render = ({ board, target, rest }) => {
range(W * H).forEach(p => {
const value = board[p];
const cell = cells[p];
const list = cell.classList;
cell.innerHTML = tileChar(value);
list.add(tileColor(value));
list.remove(
'none'
,
'wall'
,
'selected'
);
if
(value === 0) {
list.add(
'none'
);
}
else
if
(value < 0) {
list.add(
'wall'
);
}
else
if
(target === p) {
list.add(
'selected'
);
}
});
if
(prevRest !== rest) {
prevRest = rest;
if
(!rest) {
message.innerHTML =
'clear!'
;
seClear.play();
}
else
if
(!findPair(board)) {
message.innerHTML =
'game over'
;
seLose.play();
}
else
{
message.innerHTML = rest;
}
}
};
render(state);
};
main();
CSS
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 @font-face {
font-family
:
'Anton'
;
src
:
url
(Anton-Regular.ttf);
}
#demo{
font-family
:Anton;
}
#demo *{
box-sizing:border-box;
}
#demo p {
text-align
:
center
;
font-size
:
24px
;
margin
:
0
;
}
.board {
display
: table;
margin
:
0
auto
;
border-spacing
:
2px
;
width
: fit-content;
font-weight
:
bold
;
font-family
:
'メイリオ'
,Meiryo,
'MS Pゴシック'
,
sans-serif
;
}
.board > div {
display
:
table-row
;
}
.board > div:nth-child(
1
),
.board > div:nth-child(
12
) {
display
:
none
;
}
.board > div > a {
display
:
table-cell
;
vertical-align
:
middle
;
border-radius:
4px
;
height
:
4.0em
;
width
:
3em
;
border
:
2px
solid
black
;
font-weight
:
bold
;
text-align
:
center
;
cursor
:
default
;
user-select:
none
;
}
.board > div > a.wall {
background
:
black
;
visibility
:
hidden
;
}
.board > div > a.
none
{
visibility
:
hidden
;
}
.board > div > a:hover {
border
:
2px
solid
#999
;
}
.board > div > a.selected {
border
:
2px
solid
#f00
;
}
.c-r{
background
:hsl(
0
deg
0%
100%
);
}
.c-b{
background
:hsl(
90
deg
0%
85%
);
}
.c-g{
background
:hsl(
180
deg
0%
70%
);
}
.c-y{
background
:hsl(
270
deg
0%
55%
);
}
HTML
ページのソースを表示 : Ctrl+U , DevTools : F12
view-source:https://hi0a.com/demo/-js/js-game-mahjong-shisensho/
ABOUT
hi0a.com 「ひまアプリ」は無料で遊べるミニゲームや便利ツールを公開しています。
プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。
必要な機能の関数をコピペ利用したり勉強に活用できます。
プログラムの動作サンプル結果は画面上部に出力表示されています。
環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。
画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。
動く便利なものが好きなだけで技術自体に興味はないのでコードは汚いです。
途中放置や実験状態、仕様変更、API廃止等で動かないページもあります。