PC/スマホの一人回し用アプリの作り方(2日目)




 この記事は、Board Game Design Advent Calendar 2025 の17日目の記事の続きです。前回はこちらです。

目次

 1日目 準備
 2日目 APモジュール(その1)
 3日目 APモジュール(その2)
 4日目 AP実装
 5日目 PRモジュール
 6日目 PR実装(その1)
 7日目 PR実装(その2)
 8日目 スマホ対応

 本日は2日目「APモジュール(その1)」です。

はじめに

 APはアプリケーション(Application)の略で、業務処理みたいな意味合いで言っています。後半で説明するPR:プレゼンテーション(Presentation)との対比として「AP層」「PR層」という呼び方で使うことが専らで、PRはユーザに見せる画面やUIの部分という意味合いです。我々の文脈であるボードゲームではルールがAP、UI実装がPRです。で、だいたいAP側のほうが簡単なことが多いので、この記事でも先にAP層から実装していきます。AP層はゲームの説明書通りにコーディングしていけば足りる一方で、PR層はコンピュータ特有のUIを整えるのがAP層に比べて難しかったり、面倒だったりします。
 モジュールは取外し可能な部品のことです。一応moduleの意味を辞書にあたっておくと、

any one of a set of parts or units that are made separately and can be joined together to construct a building or piece of furniture.
別個に作られ、組み合わせて建物や家具の一部を構成することができる、パーツ(部品)またはユニット(部材)のセットの任意の1つ。

(Oxford Advanced Learner's Dictionary of Current English, Fifth Edition)

と、この語釈をそのままコンピュータの文脈に置き換えても違和感はないと思います。先に部品を作っておけるものは作っておいて、後で使い回せると便利でいいじゃない? ということで、共通部品を作るところから話を始めましょう。ソースコードの数が多いので、2回に分けて説明します。部品だけの話だと使い方が分かりにくいと思いますので、次回(3日目)の最後に『シャット・ザ・ボックス』の簡易な実装をします。サンプルゲームの前段としてサンプルゲームを実装するのは倒錯感あるけどしょうがないよね。

 言語はPython、筆者のバージョンは3.10.1で、OSはWindows 11です。Pythonの文法知識は説明を省略します。私もコーディングうまくないので、そんなに難しい箇所はないはずです。組んだときの体調によってdocstringやテストコードが丁寧だったり雑だったりするのは許してください。

必要なモジュールの一覧

 今回と次回のソースコードは、github.com/kazuma0221/bgmodules に全量を置いてあります。画面中央やや上にある、【 <> Code 】という緑のボタンを押してgh repo cloneするなり「Download ZIP」で落とすなりして使えます。記事中にもソースコードを載せていますので、コードエリア右上の「Copy」ボタンで個別にコピーもできます。
 第2回ではこのうち、パッケージ bgpieces を説明します。

 パッケージ構成は下記のようになっていますが、今回のシリーズで使うのは「bgpieces」「boardgame」の2つのサブパッケージだけです。「cardgame」「playingcards」は消してもかまいません。(ちなみに「cardgame」はトリテ用のAPモジュールで、「playingcards」はトランプのカードデータを作るモジュールです。トリテ版の説明は昔こちらの記事に書きましたが、古い記事のため現在のソースとは差分があることをご了承ください。)

bgmodules
 ├ bgpieces
  ├ board.py
  ├ color.py
  ├ die.py
  ├ piece.py
  └ worker.py
 ├ boardgame
  ├ eventtype.py
  ├ game.py
  ├ player.py
  ├ proc.py
  ├ procendgame.py
  ├ procstartgame.py
  ├ rules.py
  └ table.py
 ├ cardgame ※展開略
 └ playingcards ※展開略

 説明の都合上、紹介順はパッケージ内の並び(アルファベット)順とは異なります。

bgpieces:コンポーネント

 最初の board.py は、今回は便宜上作っただけでほぼデータクラスのようなものです。今後ここに共通機能を足していくと便利かな~ぐらいに思ってはいますが、当面はこのクラスを使う代わりに単なるdictでも代替できます。もし機能を足すとすれば、スペース(マス)間のコマ移動とか、外周の得点トラックにコマを置いて点数とプレイヤーの一覧を取得できるとか、そういう感じになると思います。

board.py
class Board():
'''ボードゲームのボードを表すクラス。
大きさは問わない。物を配置するスペースは必須で、名前(種類)は任意で、それぞれ持つ。
:param dict spaces: 物を配置するスペースの辞書。各スペースの型は任意で、デフォルトのコレクションでも独自クラスでもよい。
:param str name: ボードの名前。設定しなくてもよい。'''
def __init__(self, spaces:dict, name:str=None):
'''スペースの辞書と、名前(種類)をセットする。'''
self.spaces:dict = spaces
self.name:str = name



 続いて color.py です。単なるEnumのサブクラスですが機能としては重要で、コンポーネントの色(役割)や、プレイヤーを識別するのに使います。右辺の値は互いに区別がつけば何でもかまいません。strをmixinすることでstrのメソッドを直接使えるようにしており、たとえば Color.BLACK.value.casefold() と書かずに、Color.BLACK.casefold() と書けます。
 他の色も必要に応じて足してください。あまり増やすとPR層(UI)での色覚対応が大変にはなりますが、AP層の時点では関係ありません。プレイヤーカラーの色覚対応としては、白と黒、それにスプラトゥーン3のインクのデフォルトカラーである青と黄色の4色が鉄板です。行5~8で最初の4色を書くときにうっすら意識しました。

color.py
from enum import Enum

class Color(str, Enum):
'''ゲーム内の色を表す、strをmixinした列挙型。コンポーネントやプレイヤーの色に使う。'''
WHITE = 'WHITE'
BLACK = 'BLACK'
BLUE = 'BLUE'
YELLOW = 'YELLOW'
RED = 'RED'
GREEN = 'GREEN'
PINK = 'PINK'
PURPLE = 'PURPLE'
SKYBLUE = 'SKYBLUE'

def names():
'''列挙名の一覧を文字列のイテラブルで返す。'''
return Color.__members__

def items():
'''列挙したEnum値の一覧をイテラブルで返す。'''
return Color._member_map_.values()

def values():
'''列挙したEnum値の、実際の値の一覧をリストで返す。'''
return [c.value for c in Color._member_map_.values()]

# テスト
if __name__ == '__main__':
# Enumオブジェクトの文字列表現と、オブジェクト自体のペアを列挙
# タプルの左側は実際の値ではなく、左辺の変数名がstrになって返されている
for c in Color._member_map_.items():
print(c)
# 文字列表現のみ
for c in Color.names():
print(c)
# Enumオブジェクトのみ
for c in Color.items():
print(c)
# 値のみ
for c in Color.values():
print(c)



 piece.py はコマとして汎用的に使うクラスです。色はオブジェクトを作るときに決めます。大きさや形はAP側にとっては関係ないので定義していませんが、それらを問題にするゲームの場合はサブクラスで決めるといいかと思います。引数nameはコマの種類を聞くことを想定していますが、ユニークな名前を振るなど好きに使って問題ありません。
 コマと言いましたが大きさも形も関係ない単なる概念なので、チップやカード、『ツォルキン』の歯車など、何でもPieceに含められます。上で紹介したBoard、つまりゲームボードもコンポーネントとしてPieceのサブクラスに含めるかどうかは派閥の問題で、私は流儀的にはどっちでもいいのですが、コーディングしながら「board.」のサジェストにcolorやvalueが出てくるのは煩わしいと思ったので、BoardはPieceを継承していません。

piece.py
from bgpieces.color import Color

class Piece():
'''ボードゲームのコマを表すクラス。
大きさは問わない。色、値、名前(種類)を任意で持ってもよい。値、名前(種類)はどの型でもよい。'''
def __init__(self, color:Color=None, value=None, name=None):
'''色、値、名前(種類)をセットする。'''
self.color:Color = color
self.value = value
self.name = name



 die.pyはサイコロです。単数がdieで複数がdiceです。コマの一種としてPieceクラスを継承しています。サイコロは色も値もあったほうがいいでしょう。
 振るための乱数生成にはnumpyを使っています。後で画面実装するときにKivyというフレームワークと、iOSに移行するためのkivy-iosというツールを使うのですが、後者のkivy-iosではデフォルトだと使えるパッケージが限られていて、numpyはその「使えるパッケージ」の一覧に入っているありがたいモジュールです。Python標準のrandomパッケージをimportもできますがあまり良い乱数ではないらしくて、最初からnumpyを使うのが手堅いかと思って採用しました。
 『プチプチ』ではダイスは使いませんが、6面ダイスに限れば簡単に作れるので、『シャット・ザ・ボックス』の実装用に入れておきました。メソッド flip() はユーロゲーマー的にはよく使うはずです。本当は6面ダイス以外でも flip() は作れるはずですが、私がダイスの種類に不案内なため今回すぐに実装するのはやめました。

die.py
import numpy as np
from bgpieces.piece import Piece
from bgpieces.color import Color

class Die(Piece):
'''ダイスのクラス。値、面数、色を持つ。デフォルトでは1~6の6面ダイスになる。'''
def __init__(self, value:int=1, sides:int=6, color:Color=Color.WHITE):
'''初期値を設定し、乱数生成器を初期化する。
:param int value: ダイスの示す値。
:param int sides: ダイスの面数。
:param Color color: ダイスの色。ゲームに使わなければデフォルトのままでよい。'''
super().__init__(color=color, value=value, name='die')
self.sides = sides
self.rng = np.random.default_rng()

def roll(self)->int:
'''ダイスを振り、値を「1~面数のあいだの整数」でランダムに更新して返す。'''
self.value = self.rng.choice(self.sides) + 1
return self.value

def flip(self)->int:
'''ダイスを裏返す。6面ダイスの場合のみ裏返して値を返し、それ以外のダイスではNoneを返す。'''
if self.sides == 6:
self.value = 7 - self.value
return self.value
else:
return None

# テスト
if __name__ == '__main__':
die = Die()
print(f'ROLL: {die.roll()}')
print(f'FLIP: {die.flip()}')

 たとえばこれを『チャオチャオ』のダイスにしたい場合は、1の裏が4、2の裏が3、残る2面が×になります。下記は一例です。サブクラスで __init__() は変えず、roll() と flip() をオーバーライドします。どちらの×が上を向いているかが問題になる場面はないと思います。もしそこまで必要であれば、サイコロの形状をきちんとマッピングしたクラスを一から作り、それを擬似的に回転させたほうがいいです。

chaochaodie.py
from bgpieces.die import Die

class ChaoChaoDie(Die):
'''チャオチャオのダイスのクラス。6面ダイスで、×の2面が対面、残り4面は1~4で表裏の和が5になる。'''

def roll(self)->int:
'''ダイスを振り、値をランダムに更新して返す。×は0を返す。'''
self.value = self.rng.choice([1, 2, 3, 4, 0, 0])
return self.value

def flip(self)->int:
'''ダイスを裏返し、値を更新して返す。'''
if self.value > 0:
self.value = 5 - self.value
return self.value

# テスト
if __name__ == '__main__':
die = ChaoChaoDie()
print(f'ROLL: {die.roll()}')
print(f'FLIP: {die.flip()}')



 最後にworker.pyです。ワーカープレイスメントのワーカーとして使うPieceのサブクラスで、大きさの定義だけを追加しています。
 大きさは数値比較ができるようにIntEnumを使っています。大きさの概念がないゲームでも、他のPieceと区別するために内容のないサブクラスまたは独自クラスにしておくほうが無難でしょう。

worker.py
from enum import IntEnum
from bgpieces.piece import Piece
from bgpieces.color import Color

class WorkerSize(IntEnum):
'''ワーカーの大小を定義する列挙型。'''
BIG = 3
MEDIUM = 2
SMALL = 1

class Worker(Piece):
'''ワーカーのクラス。色と大きさを持たせる。'''
def __init__(self, color:Color, size:WorkerSize=WorkerSize.SMALL):
super().__init__(color)
self.size = size



 執筆時点では、カードのクラスを作っていません。カードに持たせる属性がゲームによってバラバラで、共通機能の抽出に悩んだからですが、要らないとは思っていないので後日ここに追加するかと思います。
 というわけで今回はコンポーネントを組みました。3日目「APモジュール(その2)」では、ルールを回すための部品を組んでいきます。年内の完結はとうに諦めました。



<2025/12/27>


←No.55 PC/スマホの一人回し用アプリの作り方(1日目)
コラム一覧へ トップページへ