あけましておめでとうございます。
毎年ながら、相当暇なときにしか記事を書いていませんが、今年も例の如くネタを提供していければと思います。
今回は、以前、リアルボードゲームでいくつか興味を持ったゲームを、コロナのリモート時代にオンラインで楽しめるように、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つ用意します。
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枚になるように補充しています
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単位のカードはどんな数字でも出せる
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/
こちらにアクセスして、以下の通りゲームを進めてください
- 「ニックネーム」を入力する
- 「ゲームを作る」ボタンを押す
- この時点で一人で遊ぶ場合は「ゲームを始める」ボタンを押す。複数人で遊びたい場合は、「ゲームを作る」ボタンの下にあるリンクをメンバーに共有する
- 共有された人は、「ニックネーム」を入力後、「ゲームに参加する」ボタンを押す
- 必要な人数が揃ったら「ゲームを始める」ボタンを押す
注意事項:ゲームが始まったら、F5キーや、ブラウザの再読み込みは行わないでください。行った場合は、最初からやり直してください。