遊び方・ゲームルール

四川省とは麻雀牌を使ったパズルゲーム

2つずつ牌を取り除く。牌を取り除けるのは以下の条件のときである。

四川省 ゲームルール 2回まで曲げて繋げられる

同種の2つの牌が隣接しているとき

同種の2つの牌を他の牌のない場所を通る水平・鉛直の直線で結ぶ際、その線が曲がる回数が2回以内のとき。

これを繰り返して牌を取り除いていく。すべての牌を取り除くと勝ちとなる。

牌を取り除く順によっては終了できず手詰まりとなることもある。

麻雀を知らない人でもとっつきやすいよう表記を英数字や記号にしています。

動画でみる

上海

view source

JavaScript

//https://codepen.io/nagtkk/pen/RwwpKmj
//https://qiita.com/nagtkk/items/ba720840328185e2cb91

//アルゴリズムと記法の勉強

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

@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(0deg 0% 100%);
}
.c-b{
  background:hsl(90deg 0% 85%);
}
.c-g{
  background:hsl(180deg 0% 70%);
}
.c-y{
  background:hsl(270deg 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廃止等で動かないページもあります。