ボードゲームをWEB化してみたシリーズ＜THE GAME＞

あけましておめでとうございます。
毎年ながら、相当暇なときにしか記事を書いていませんが、今年も例の如くネタを提供していければと思います。

今回は、以前、リアルボードゲームでいくつか興味を持ったゲームを、コロナのリモート時代にオンラインで楽しめるように、Herokuを使ってPythonで作ってみました。

実は、すでに10種類ぐらい作っているので、徐々に公開していければと思います。
（なお、これはあくまでも個人の趣味であり、完全にゲームを再現できているわけではありません。）

# まずはじめに

職場でボードゲームをしていたが、諸事情と、コロナ禍になり直接会ってゲームすることが難しくなりました。ただ、その中でもボードゲームをやりたいなー。と思い、WEBでできればみんなで楽しめるんじゃないか？！と思い、関係者限定公開で開発しました。
「THE GAME」については、ゲームはとてもシンプルで、一人からでも楽しめるので、最初に作ってみました。

# 動作環境

Heroku + Flask with Python + JQuery(Ajax) + Redis

# 「THE GAME」とは

実際にゲームをお持ちでない方は、下記を参考にしてみてください。
https://bodoge.hoobby.net/games/the-game

# 仕組み

基本構成としては、Flaskをベースとしたアプリケーションですが、REST API設計にしました。
ゲーム画面はHTML/JS/CSSで構成しますが、すべての操作はAjaxで、サーバ側のREST APIを呼び出し、その結果を加工して使うようにしています。
Redisは、ゲーム中のゲームマスターと、クライアントの紐付け管理や、ゲームの進行内容を管理するために使用しています
※細かい技術については、様々な記事からのピックアップなので省略します

# ゲームの分析

まずはゲームを分析・理解し、どのように仕組み化するかを考えます。

## ゲームの全容

- テーブル上には、山場と、4つのカード置き場
- カードは2から99までの1組（計98枚のカード）

## ゲームのルール

- 最初にプレイヤーに配る手札の数は、人数により調整
- 最初に4つのカード置き場には、1→99というカードと、100→2というカードを2枚ずつ、合計4枚配置
- プレイヤーは2枚以上カードを出す必要がある（2枚以上なら手札分を全部おいてもよし）
- 配置した4枚のカードには、1→99の場合には、直前に出されているカードより大きい数字のカードを置く
- 100→2の場合には、直前に出されているカードより小さい数字のカードを置く
- プレイヤーのターンが終わったら、出したカード分を山札から補充する
- 特殊ルール「10もどし」：これは直前に出されているカードより10小さい/大きいカードを出すことが可能

## 勝利条件

勝利条件は下記の通りありますが、これは枚数を見て判断すればいいので、機能は実装しません

- 9枚以下なら「勝利」
- 0枚なら「完全勝利」
- 10枚以上なら「敗北」

## 最重要ルール
このゲームは、協力ゲームであり、みんなで協力して手持ちのカードなるべく出せるようにすることが大事です。
しかし、ゲームの最大のルール。出すカードの数字などを発言してはいけません。
「この列にはこれ以上出さないで」とか、「この列を進めちゃってもいい？」などでゲームを進めることが大事です。
コミュニケーションが大事なので、この辺の機能も実装はしません。

# 実装部分

## まずはゲームはじめ

ここではゲームの初期化をしています。
最初に、2から99までの98枚のカードデータを作ります。
playerにはプレイヤー数分のデータが入っています。
プレイヤー数に応じて、最初に作った98枚のカードデータを書くユーザ6枚になるように各プレイヤーの配列に設定しています。
最後に、場のカードを設定しています。high->low/low->highの通りに対して、更に1を2つ、100を2つで計4つ用意します。

```python
    game['stocks'] = list(range(2, 99))

    # playerids = [player['playerid'] for player in game['players']]
    routelist = copy.copy(game['players'])
    random.shuffle(routelist)
    game['routelist'] = routelist

    players = game['players']

    for player in players:
        player['holdcards'] = []
        while len(player['holdcards']) < 6:
            player['holdcards'].append(game['stocks'].pop(random.randint(0, len(game['stocks']) - 1)))

    game['hightolow'] = []
    game['lowtohigh'] = []
    game['hightolow'].append([100])
    game['hightolow'].append([100])
    game['lowtohigh'].append([1])
    game['lowtohigh'].append([1])
```

## 次に、プレイヤーのターンエンド

game['routeidx']とはプレイヤーの順番を示し、これを1つ動かします
その上で、プレイヤーに対し、手札が6枚になるように補充しています

```python
    game['routeidx'] = (game['routeidx'] + 1) % len(game['players'])

    players = game['players']

    # refresh holdcards for all members
    for player in players:
        while len(player['holdcards']) < 6:
            if len(game['stocks']) > 0:
                player['holdcards'].append(game['stocks'].pop(random.randint(0, len(game['stocks']) - 1)))
            else:
                break
```

## カードを配置する処理

この処理が、このプログラム最長の処理になります。
カードをどの位置に配置し、それが妥当であるのかを判定する処理です。
この処理の中で、「10もどし」の判定も入れています。

そして、実は「10もどし」以外に、オリジナルルールを2つ組み込んでいます。

- 直前のカードがゾロ目なら、手持ちのゾロ目を出せる
- 直前のカードが10単位のカードの場合、手持ちの10単位のカードはどんな数字でも出せる

```python
    if lineid in [0, 1]:
        highToLow = game['hightolow'][lineid]
        # 100 -> 2
        if highToLow[-1] > cardnum:
            # highToLow.append(cardnum)
            isHit = True
        if (highToLow[-1] + 10) == cardnum and isHit == False:
            # highToLow.append(cardnum)
            isHit = True
        if game['rule'] == 'original' and isHit == False:
            if len(str(cardnum)) > 1 and len(str(highToLow[-1])) > 1:
                cardnum_str = str(cardnum)
                latest_str = str(highToLow[-1])
                if cardnum_str[0] == cardnum_str[1] and latest_str[0] == latest_str[1]:
                    # highToLow.append(cardnum)
                    isHit = True
            if highToLow[-1] % 10 == cardnum % 10:
                # highToLow.append(cardnum)
                isHit = True
        if isHit == False:
            return 'Error1'
        else:
            highToLow.append(cardnum)
    elif lineid in [2, 3]:
        lowToHigh = game['lowtohigh'][lineid%2]
        # 1 -> 99
        if lowToHigh[-1] < cardnum:
            # lowToHigh.append(cardnum)
            isHit = True
        if (lowToHigh[-1] - 10) == cardnum and isHit == False:
            # lowToHigh.append(cardnum)
            isHit = True
        if game['rule'] == 'original' and isHit == False:
            if len(str(cardnum)) > 1 and len(str(lowToHigh[-1])) > 1:
                cardnum_str = str(cardnum)
                latest_str = str(lowToHigh[-1])
                if cardnum_str[0] == cardnum_str[1] and latest_str[0] == latest_str[1]:
                    # lowToHigh.append(cardnum)
                    isHit = True
            if lowToHigh[-1] % 10 == cardnum % 10:
                # lowToHigh.append(cardnum)
                isHit = True
        if isHit == False:
            return 'Error2'
        else:
            lowToHigh.append(cardnum)
    else:
        return 'Error'

```

# 最後に
と言うことで、部分的にソースの共有だけさせていただきました。
まぁ、他にも時間をかけて実装を見直すこともできるとは思いますが、効率よく、最低限の要件を実現したところ、こんな感じになりました。

# ゲームを試してみたい方へ
https://yanyan-the-game.herokuapp.com/
こちらにアクセスして、以下の通りゲームを進めてください

1. 「ニックネーム」を入力する
2. 「ゲームを作る」ボタンを押す
3. この時点で一人で遊ぶ場合は「ゲームを始める」ボタンを押す。複数人で遊びたい場合は、「ゲームを作る」ボタンの下にあるリンクをメンバーに共有する
4. 共有された人は、「ニックネーム」を入力後、「ゲームに参加する」ボタンを押す
5. 必要な人数が揃ったら「ゲームを始める」ボタンを押す

注意事項：ゲームが始まったら、F5キーや、ブラウザの再読み込みは行わないでください。行った場合は、最初からやり直してください。