情碁 - 碁石の状態を表情で伝える囲碁ゲームシステム
どういう物?
囲碁ゲームシステムです。「情碁」と名付けました。コンピュータ思考ルーチンはなく、先手も後手も人が打ちます。碁石にディスプレイが付いていて、先手が打つと黒石に、後手が打つと白石になります。各碁石の状態に応じて表情が変わり、全体の形勢を容易に判断することができます。囲碁のルールを知らない人でも簡単にルールを理解することができます。
ウェブゲームの一つの「ぷよ碁」からインスピレーションを得ました。ぷよ碁はウェブブラウザ上でマウスを操作して碁石を打ちます。これが実際に触れることができる碁石に表情があったら面白いと思い作ってみました。
プレイの様子は以下をご覧ください。
これIoTなの?
IoTはInternet of Thingsなので、「インターネットに接続された何か」がIoTです。この作品はSPI, I2C, CANと色々な通信方法を経由して複数のモジュールが同時に通信しています。つまり、この作品はネットワークとネットワークを繋ぐものという元の意味の An inter-network を構成しています。ということでIoTです(強弁)。Raspberry PiはEthernetやWi-Fiのインターフェースを持っていますので、みなさんがよく知るインターネットであるThe Netへの拡張も可能です。
システム構成
このシステムの全体構成は以下の通りです。
また、各モジュールは以下のような構成となっています。
-
ゲーム管理装置 x1
ゲーム全体を管理します。CANを用いて碁盤モジュールと通信し、碁石が置かれたかどうかの確認や、碁石の表情の指示などを行います。- Rasbperry Pi 4 2GB
- Raspberry Pi 7インチ公式タッチディスプレイ
- WAVESHARE Raspberry Pi用 2チャンネル絶縁CAN HAT
-
碁盤モジュール x25 (五路盤の場合)
碁盤の格子にあたる部分が一つ一つモジュールになっています。碁石が置かれたかどうかや碁石がタップされたことをゲーム管理装置に通知したり、ゲーム管理装置から通知された表情更新を碁石に伝えます。- XIAO RP2040
- MCP2515 CAN Controller
- MCP2652 CAN Driver
-
碁石 x25 (五路盤の場合)
碁石が置かれたことや碁石にタップされたことを碁盤に伝えます。碁盤モジュールから表情変更指示を受け取り表情を変えます。- XIAO RP2040
- XIAO Round Display
ハイライト
多数の汎用的な技術を組み合わせて制作していて、特定の技術に焦点を当てていません。そのような中でこの作品がどのような特徴を持つかを説明します。
碁石のバッテリの搭載可否検討
碁石の通信を無線にするのであれば、物理的な端子をなくしたいところです。そうなると、バッテリを積む必要がありますが、以下の観点で難しいと判断しました。
- バッテリ容量が小さく、頻繁に充電が必要になる。
- 多量の碁石を同時に充電するオペレーションが難しい。
- バッテリのライフサイクル管理が難しい。
- バッテリを積むと碁石のサイズが大きくなる。
- 子供が触るおもちゃにバッテリを使うのは適切か?飲み込まないにしてもかじる可能性はある。
電力線を物理端子で接続するなら、通信線もそれに併せて接続すればよくなるので、碁石と碁盤モジュールは物理端子による有線接続となりました。
拡張性
今回の作品は五路盤で制作しましたが、本システムは容易に七路盤、九路盤や6x8路盤のような変則碁盤も簡単に構成できます。全ての碁盤モジュールは一つのバスにぶら下がるように設計していますので、バスの電気的な特性の許容範囲までは増設することが出来ます。碁石一つ分の碁盤モジュールの基板は50mm x 50mmとし、この碁盤モジュールのバスと電力線をピンヘッダとピンソケットで数珠つなぎ状に接続することで多数の碁盤モジュールを接続できるようにしました。
一列に繋がる碁盤モジュールを正方形状に配列させるために碁盤の左右両端にバスおよびGNDを上下に接続する補助基板を使っています。この補助基板から電力供給も行っています。上下方向に接続する補助基板で電力供給線を分割することが出来、横方向一列の碁盤モジュールに供給する電流量を制限することが出来ます。一つの碁盤モジュールとその上に載せられる碁石で併せて100mAほどの電流を必要とします。本作品においては電力供給を3つに分割することで、1つの電力線が最大1Aとなるようにしています。
同期性
実現したかった機能の一つとして、どれか一つの碁石をタップすると、その碁石を含むすべての連絡する碁石が点滅する、という機能があります。各碁石が表情を変えたり、点滅させたりするためにゲーム管理装置と通信する必要がありますが、この同期性をどう実現するかが課題になりました。
無線方式は同報通信が可能ですが、先に述べたバッテリ問題の他に、展示会などの会場での混線を考慮して、採用を見送りました。有線方式としては、SPI, I2C, RS485, CANが候補に上がります。同報通信の実装を考えるとSPIとI2Cが候補から外れます。RS485で碁盤モジュールとの個別通信と同報通信の両方を実装できますが、それらを実現するためのプロトコルを実装しなければなりません。この処理がRP2040の負担となり、同期性に問題が生じる可能性があります。
最後のCANは各碁盤モジュールへの個別通信と同報通信の両方を満たすプロトコルがCANコントローラに実装されています。RP2040への負荷が軽く、同期性の問題が生じる可能性が最も少ないと判断して採用しました。各碁盤モジュールにCANコントローラのMCP2515を実装するため単価は高くなりますが、今後XIAO RP2040の代わりにXIAO RA4M1を採用することで、CANコントローラを省略できコストも低減できる見込みです。
碁盤モジュールには碁盤の縦横の位置に合わせてXYの固有IDが振られているので、これを用いて、以下のようなCAN IDを使って通信します。
- 0x4XY: ゲーム管理装置から碁石の表情を変える指示をしたり、点滅させる準備をさせる指示をする。
- 0x6XY: 碁石が置かれたり取り除かれたりしたときや、碁石がタップされたことをゲーム管理装置に伝える。
- 0x1FF: 点滅する準備ができているすべての碁石に対して、点滅指示をする。
この通信により、同期ずれをプレイヤーに知覚されることのない点滅を実現しています。
碁石の活線挿抜
碁盤モジュールが碁石が置かれたことを検知する方法としてGPIOを使います。碁盤モジュール側のGPIOはプルアップし、割り込み処理で待ち受けます。GPIOがHIGHならば碁石なし、LOWならば碁石ありとなります。碁石側はピンソケットの接続により電力供給が開始されるとディスプレイの初期化などを行います。一通りの準備が出来たところで、碁石のGPIOをLOWにして、碁盤モジュール側に碁石が置かれたことを通知します。
碁石を置いたり外したりする操作の中でチャタリングのような事象が発生します。碁盤モジュールでGPIOの割り込み処理としては、HIGH/LOWの変化を捉えることとし、最初の変化から500m秒待機しもう一度GPIOの値を確認して碁石の着脱を判断します。待機時間中はGPIOの変化に関する割り込みは無視します。これにより、碁盤モジュールが安定して碁石の着脱を判定できるようになりました。
碁石の形状とピンヘッダ
碁石は円柱形をしています。接続するピンヘッダは碁石下部についていますが、ピンが指に刺さらないように碁石のケースを造形しています。ピンヘッダは中央より少し下に配置し、逆方向に刺しづらくしてあります。碁石が碁盤モジュールに簡単に装着出来るようにするために、ピンヘッダのある口の部分を45度の角度をつけて、碁石を置くときに碁石を回転させるとインターフェースがかみ合う場所が容易に分かるようにしました。実際に碁石を置いてみると、ピンソケットが碁石に吸い込まれるような感じで噛み合います。
考察
IoTの理想形の一つ
IoTの未来予想の一つは、すべての物事の変化がネットワークに蓄積され、それに応じてすべての物事に関与する、というものだと思います。そういう意味で、碁石のような石っころもネットワークに接続され、状態を通知したり、状態に応じた反応をしたりするということを実現しました。この作品は「あらゆるものがつながる世界」の体現の一つになったのではないかと思います。展示会参加者の意見として、実際にこういう大量のありふれたものがIoT化することにより、他ゲームや他領域の創作活動に新たなインスピレーションを与えることができる、というものがありました。
身体性
うまく数値で証明することはできませんが、実際に展示を行ってみると、ウェブゲームとは違うフィードバックが展示会参加者から返ってきました。マウスを使ってウェブブラウザを操作することでゲームを進行させることはできますが、手に取ったものが直接情報提供してくれるという身体性が意味を持っていると実感しました。この作品では、囲碁の状態評価のみが碁石の表情を変えますが、碁石の持ち方で碁石の表情が変わるような機能を持たせることができるなら、プレイヤの碁石に対する態度ももっと変わる可能性があります。
お願い
本作品の一部の機能は特許申請中です。皆さんが本作品を複製したり、改良したり、また、そのことについて記事を執筆したりすることはCC BY-NC-SA 4+のライセンスの下で自由に行えます。もし、制作物を頒布したり、販売したりするなどの商業活動をする場合は、弊社宛にご相談ください。商業活動の様態に応じてライセンス契約を結ばせていただきます。
特許出願中:特願2024-101272
付録
回路図
碁盤モジュール回路図
碁石回路図
ソースコード
ゲーム管理装置
emogo.py
import threading
import time
import can
import sys
class Stone:
# 定数の定義
COLOR_VALUES = {
'black': 0x01,
'white': 0x02
}
DIRECTION_VALUES = {
'north': 90,
'east': 180,
'south': 270,
'west': 0
}
EMOTION_VALUES = {
'dead': 0,
'defensive': 1,
'normal': 2,
'offensive': 3
}
def __init__(self):
self.color = None
self.direction = self.DIRECTION_VALUES['north']
self.emotion = self.EMOTION_VALUES['normal']
# 色を設定するメソッド
def set_color(self, color):
if color in self.COLOR_VALUES:
self.color = self.COLOR_VALUES[color]
elif color in self.COLOR_VALUES.values():
self.color = color
else:
raise ValueError("Invalid color")
# 色を取得するメソッド
def get_color(self):
return self.color or 0x00 # 0x00 は石がないことを示す
# 向きを設定するメソッド
def set_direction(self, direction):
if direction in self.DIRECTION_VALUES:
self.direction = self.DIRECTION_VALUES[direction]
elif direction in self.DIRECTION_VALUES.values():
self.direction = direction
else:
raise ValueError("Invalid direction")
# 向きを取得するメソッド
def get_direction(self):
return self.direction or 0
# 感情を設定するメソッド
def set_emotion(self, emotion):
if emotion in self.EMOTION_VALUES:
self.emotion = self.EMOTION_VALUES[emotion]
elif emotion in self.EMOTION_VALUES.values():
self.emotion = emotion
else:
raise ValueError("Invalid emotion")
# 感情を取得するメソッド
def get_emotion(self):
return self.emotion or self.EMOTION_VALUES['dead']
class Board:
def __init__(self, n, m):
self.n = n # 行数
self.m = m # 列数
self.board = [[None for _ in range(m)] for _ in range(n)] # [行][列] の順序
self.connect = []
# 石を置くメソッド
def place_stone(self, x, y, color):
if 1 <= x <= self.n and 1 <= y <= self.m:
ix = x - 1 # 行インデックス
iy = y - 1 # 列インデックス
if self.board[ix][iy] is None:
stone = Stone()
stone.set_color(color)
stone.set_direction('north')
stone.set_emotion('normal')
self.board[ix][iy] = stone
# ここで死に石判定を行う
self.check_dead_stones_after_placement(x, y, color)
else:
raise RuntimeError(f"Position ({x}, {y}) already has a stone")
else:
raise IndexError(f"Position ({x}, {y}) is out of bounds")
# 石を取り除くメソッド
def remove_stone(self, x, y):
if 1 <= x <= self.n and 1 <= y <= self.m:
ix = x - 1 # 行インデックス
iy = y - 1 # 列インデックス
if self.board[ix][iy] is not None:
self.board[ix][iy] = None
else:
raise RuntimeError(f"No stone at position ({x}, {y}) to remove")
else:
raise IndexError(f"Position ({x}, {y}) is out of bounds")
# 石を取得するメソッド
def get_stone(self, x, y):
if 1 <= x <= self.n and 1 <= y <= self.m:
ix = x - 1 # 行インデックス
iy = y - 1 # 列インデックス
return self.board[ix][iy]
else:
raise IndexError(f"Position ({x}, {y}) is out of bounds")
# 死に石判定を行うメソッド
def check_dead_stones_after_placement(self, x, y, color):
opponent_color = 'white' if color == 'black' else 'black'
# 1. 相手の死に石を判定し、感情を 'dead' に設定
opponent_dead_stones = self.find_dead_stones(opponent_color)
for ix, iy in opponent_dead_stones:
stone = self.board[ix][iy]
if stone:
stone.set_emotion('dead')
# 2. 自分の死に石を判定し、感情を 'dead' に設定
self_dead_stones = self.find_dead_stones(color)
for ix, iy in self_dead_stones:
stone = self.board[ix][iy]
if stone:
stone.set_emotion('dead')
# 3. 繋がりと感情の更新
self.check_connect()
# 死に石を見つけるメソッド
def find_dead_stones(self, color):
visited = [[False for _ in range(self.m)] for _ in range(self.n)]
dead_stones = []
for ix in range(self.n):
for iy in range(self.m):
if visited[ix][iy]:
continue
stone = self.board[ix][iy]
if stone is None or stone.get_color() != Stone.COLOR_VALUES[color]:
continue
group, liberties = self.dfs(ix, iy, color, visited)
if liberties == 0:
dead_stones.extend(group)
return dead_stones
# 深さ優先探索でグループと呼吸点を計算
def dfs(self, ix, iy, color, visited):
stack = [(ix, iy)]
group = []
liberties = set() # 呼吸点をセットで管理
visited[ix][iy] = True
while stack:
x, y = stack.pop()
group.append((x, y))
neighbors = [
(x - 1, y), # 上
(x + 1, y), # 下
(x, y - 1), # 左
(x, y + 1) # 右
]
for nx, ny in neighbors:
if 0 <= nx < self.n and 0 <= ny < self.m:
neighbor_stone = self.board[nx][ny]
if neighbor_stone is None or neighbor_stone.get_emotion() == Stone.EMOTION_VALUES['dead']:
liberties.add((nx, ny)) # 呼吸点をセットに追加
elif neighbor_stone.get_color() == Stone.COLOR_VALUES[color] and not visited[nx][ny]:
visited[nx][ny] = True
stack.append((nx, ny))
return group, len(liberties)
# 連のためのDFS(死に石も含む)
def dfs_for_connect(self, ix, iy, color, visited):
stack = [(ix, iy)]
group = []
liberties = set()
visited[ix][iy] = True
while stack:
x, y = stack.pop()
group.append((x, y))
neighbors = [
(x - 1, y), # 上
(x + 1, y), # 下
(x, y - 1), # 左
(x, y + 1) # 右
]
for nx, ny in neighbors:
if 0 <= nx < self.n and 0 <= ny < self.m:
if not visited[nx][ny]:
neighbor_stone = self.board[nx][ny]
if neighbor_stone is not None and neighbor_stone.get_color() == color:
visited[nx][ny] = True
stack.append((nx, ny))
elif neighbor_stone is None or neighbor_stone.get_emotion() == Stone.EMOTION_VALUES['dead']:
liberties.add((nx, ny))
return group, len(liberties)
# 連と感情を再計算するメソッド
def check_connect(self):
self.connect = []
visited = [[False for _ in range(self.m)] for _ in range(self.n)]
for ix in range(self.n):
for iy in range(self.m):
if visited[ix][iy]:
continue
stone = self.board[ix][iy]
if stone is None:
continue # 死に石も含めるため、感情チェックを削除
color = stone.get_color()
group, liberties_count = self.dfs_for_connect(ix, iy, color, visited)
# 感情の設定
emotion = 'normal'
if liberties_count == 1:
emotion = 'defensive'
elif liberties_count == 0:
emotion = 'dead'
for gx, gy in group:
self.board[gx][gy].set_emotion(emotion)
self.connect.append(group)
# 指定された位置の石と連絡している石を取得するメソッド
def get_connect(self, x, y):
if 1 <= x <= self.n and 1 <= y <= self.m:
ix = x - 1 # 行インデックス
iy = y - 1 # 列インデックス
start_stone = self.board[ix][iy]
if start_stone is None:
raise RuntimeError(f"No stone at position ({x}, {y})")
else:
color = start_stone.get_color()
connected_stones = self.dfs_get_connected_stones(ix, iy, color)
return [(cx + 1, cy + 1) for cx, cy in connected_stones]
else:
raise IndexError(f"Position ({x}, {y}) is out of bounds")
# 深さ優先探索で連絡している石を取得するメソッド(死に石も含む)
def dfs_get_connected_stones(self, ix, iy, color):
stack = [(ix, iy)]
connected = []
visited = [[False for _ in range(self.m)] for _ in range(self.n)]
visited[ix][iy] = True
while stack:
x, y = stack.pop()
connected.append((x, y))
neighbors = [
(x - 1, y), # 上
(x + 1, y), # 下
(x, y - 1), # 左
(x, y + 1) # 右
]
for nx, ny in neighbors:
if 0 <= nx < self.n and 0 <= ny < self.m:
if not visited[nx][ny]:
neighbor_stone = self.board[nx][ny]
if neighbor_stone is not None and neighbor_stone.get_color() == color:
visited[nx][ny] = True
stack.append((nx, ny))
return connected
# 石の数を数えるメソッド
def stone_counts(self):
black_count = 0
white_count = 0
for row in self.board:
for stone in row:
if stone and stone.get_emotion() != Stone.EMOTION_VALUES['dead']:
if stone.get_color() == Stone.COLOR_VALUES['black']:
black_count += 1
elif stone.get_color() == Stone.COLOR_VALUES['white']:
white_count += 1
return {'black': black_count, 'white': white_count}
# ボードの状態を取得するメソッド
def get_board_state(self):
state = []
for ix, row in enumerate(self.board):
for iy, stone in enumerate(row):
if stone:
x = ix + 1
y = iy + 1
state.append({
'x': x,
'y': y,
'color': stone.get_color(),
'emotion': stone.get_emotion(),
'direction': stone.get_direction()
})
return state
# 指定した碁石の状態(感情、向き)を直接変更するメソッド
def set_stone_state(self, x, y, emotion=None, direction=None):
if 1 <= x <= self.n and 1 <= y <= self.m:
ix = x - 1 # 行インデックス
iy = y - 1 # 列インデックス
stone = self.board[ix][iy]
if stone is not None:
if emotion is not None:
stone.set_emotion(emotion)
if direction is not None:
stone.set_direction(direction)
else:
raise RuntimeError(f"No stone at position ({x}, {y}) to set state")
else:
raise IndexError(f"Position ({x}, {y}) is out of bounds")
class CANInterface:
def __init__(self, channel='can0', bustype='socketcan'):
self.emogo = None
self.bus = can.interface.Bus(channel=channel, bustype=bustype)
self.receive_thread = threading.Thread(target=self.receive_messages)
self.receive_thread.daemon = True # Daemon thread
self.receive_thread.start()
# CANメッセージを受信するメソッド
def receive_messages(self):
while True:
message = self.bus.recv()
if message is not None:
can_id = message.arbitration_id
data = message.data
self.process_message(can_id, data)
# メッセージを処理するメソッド
def process_message(self, can_id, data):
# CAN ID から石の位置を取得
if (can_id & 0xF00) == 0x600:
x = (can_id & 0x0F0) >> 4
y = can_id & 0x00F
# データの解釈
action = data[0]
if action == 0: # 取り除かれた
self.emogo.handle_stone_removed(x, y)
elif action == 1: # 置かれた
self.emogo.handle_stone_placed(x, y)
elif action == 2: # タップされた
self.emogo.handle_stone_tapped(x, y)
else:
print(f"Unknown action: {action}")
else:
print(f"Unknown CAN ID: 0x{can_id:X}")
# メッセージを送信するメソッド
def send_message(self, can_id, data):
message = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
try:
self.bus.send(message)
# メッセージ内容を表示します
data_bytes = ' '.join(f"{byte:02X}" for byte in data)
print(f"{can_id:03X}#{data_bytes}")
except can.CanError as e:
print(f"Error sending CAN message: {e}")
# Emogo インスタンスを設定するメソッド
def set_emogo(self, emogo):
self.emogo = emogo
class Emogo:
def __init__(self, board_size_n=9, board_size_m=9):
self.board = Board(board_size_n, board_size_m)
self.can_interface = CANInterface()
self.current_player = 'black' # 黒石が先攻
self.game_over = False
self.waiting_for_dead_stones_removal = False # 死に石の除去待ちフラグ
self.dead_stones_list = [] # 死に石のリスト
self.can_interface.set_emogo(self) # CANInterfaceにEmogoのインスタンスを設定
self.consecutive_passes = 0 # 連続パス回数
self.input_thread = threading.Thread(target=self.handle_keyboard_input)
self.input_thread.daemon = True # Daemon thread
self.input_thread.start()
# ゲームを開始するメソッド
def start_game(self):
print("Game started! Black goes first.")
self.game_thread = threading.Thread(target=self.game_loop)
self.game_thread.daemon = True # Daemon thread
self.game_thread.start()
# ゲームループ
def game_loop(self):
while not self.game_over:
time.sleep(0.1) # 100ms 待機
# キーボード入力を処理するメソッド
def handle_keyboard_input(self):
while not self.game_over:
user_input = input()
if user_input.lower() == 'pass':
self.handle_pass()
elif user_input.lower() == 'quit':
print("Game terminated by user.")
self.game_over = True
sys.exit()
else:
print("Unknown command. Type 'pass' to pass your turn or 'quit' to exit.")
# パスを処理するメソッド
def handle_pass(self):
if self.waiting_for_dead_stones_removal:
print("Cannot pass while waiting for dead stones to be removed.")
return
print(f"{self.current_player.capitalize()} passed.")
self.consecutive_passes += 1
if self.consecutive_passes >= 2:
print("Both players have passed consecutively. The game is over.")
self.game_over = True
self.calculate_final_score()
else:
self.switch_player()
# 石が置かれたことを処理するメソッド
def handle_stone_placed(self, x, y):
if self.game_over:
print("Game is over. No more moves can be made.")
return
try:
if self.waiting_for_dead_stones_removal:
# 死に石除去待ちの場合、新しい石は死に石とする
self.board.place_stone(x, y, self.current_player)
# 直接その石の状態を変更
self.board.set_stone_state(x, y, emotion='dead')
# 追加された石を死に石リストに追加
self.dead_stones_list.append((x, y))
# 更新した石の状態を送信
stone = self.board.get_stone(x, y)
stone_info = {
'x': x,
'y': y,
'color': stone.get_color(),
'emotion': stone.get_emotion(),
'direction': stone.get_direction()
}
self.send_stone_update(stone_info)
self.display_board(self.board)
print(f"Stone placed at ({x}, {y}) is immediately dead.")
else:
color = self.current_player
self.board.place_stone(x, y, color)
self.update_board_state()
self.display_board(self.board)
print(f"{self.current_player.capitalize()} placed a stone at ({x}, {y}).")
self.consecutive_passes = 0 # パス回数をリセット
self.check_for_dead_stones()
if not self.waiting_for_dead_stones_removal:
self.switch_player()
except Exception as e:
print(f"Error: {e}")
# 石が取り除かれたことを処理するメソッド
def handle_stone_removed(self, x, y):
try:
self.board.remove_stone(x, y)
# 石が取り除かれたら、死に石リストから削除
if (x, y) in self.dead_stones_list:
self.dead_stones_list.remove((x, y))
self.display_board(self.board)
print(f"Stone at ({x}, {y}) was removed.")
# 死に石リストが空か確認
if not self.dead_stones_list:
self.waiting_for_dead_stones_removal = False
print("All dead stones have been removed. Game resumes.")
self.switch_player()
else:
# まだ死に石が残っている場合、リストを表示
print("Please remove the remaining dead stones:")
for x_remain, y_remain in self.dead_stones_list:
print(f"- Stone at ({x_remain}, {y_remain})")
except Exception as e:
print(f"Error: {e}")
# 石がタップされたことを処理するメソッド
def handle_stone_tapped(self, x, y):
try:
print(f"Stone at ({x}, {y}) was tapped.")
connected_stones = self.board.get_connect(x, y)
if connected_stones:
# タップされた石と連絡する全ての石にメッセージを送信
for stone_x, stone_y in connected_stones:
can_id = 0x400 | (stone_x << 4) | stone_y
data = [0x02, 0xFF]
self.can_interface.send_message(can_id, data)
# 0.5秒待機してから全石に対してメッセージを送信
def blink_stones():
for _ in range(3):
time.sleep(0.5)
# 全石に対してメッセージを送信(ブロードキャスト)
can_id = 0x1FF
data_on = [0x03, 0xFF]
data_off = [0x03, 0x00]
self.can_interface.send_message(can_id, data_on)
time.sleep(0.5)
self.can_interface.send_message(can_id, data_off)
# 最後に連絡する各石にメッセージを送信
for stone_x, stone_y in connected_stones:
can_id = 0x400 | (stone_x << 4) | stone_y
data = [0x02, 0x00]
self.can_interface.send_message(can_id, data)
# 点滅処理を別スレッドで実行
threading.Thread(target=blink_stones).start()
else:
print(f"No connected stones found for ({x}, {y})")
except Exception as e:
print(f"Error handling stone tap: {e}")
# 死に石があるかチェックし、処理を行う
def check_for_dead_stones(self):
dead_stones = self.get_dead_stones()
if dead_stones:
self.waiting_for_dead_stones_removal = True
self.dead_stones_list = dead_stones.copy()
print("Dead stones detected. Please remove the following stones:")
for x, y in dead_stones:
print(f"- Stone at ({x}, {y})")
else:
self.waiting_for_dead_stones_removal = False
self.dead_stones_list = []
# 死に石のリストを取得する
def get_dead_stones(self):
dead_stones = []
board_state = self.board.get_board_state()
for stone_info in board_state:
if stone_info['emotion'] == Stone.EMOTION_VALUES['dead']:
dead_stones.append((stone_info['x'], stone_info['y']))
return dead_stones
# ボードの状態を更新し、各石に通知するメソッド
def update_board_state(self):
# Boardの状態を更新(感情などの再計算はBoard内で行われる)
stone_counts = self.board.stone_counts()
print(f"Black stones: {stone_counts['black']}, White stones: {stone_counts['white']}")
# 各石に状態を通知
board_state = self.board.get_board_state()
for stone_info in board_state:
self.send_stone_update(stone_info)
# 石に状態を送信するメソッド
def send_stone_update(self, stone_info):
can_id = 0x400 | (stone_info['x'] << 4) | stone_info['y']
command = 1 # 状態変更の命令コードは1
color = stone_info['color']
emotion = stone_info['emotion']
direction = stone_info['direction']
direction_high = (direction >> 8) & 0xFF
direction_low = direction & 0xFF
data = [
command & 0xFF,
color & 0xFF,
emotion & 0xFF,
direction_high,
direction_low
]
self.can_interface.send_message(can_id, data)
# プレイヤーを交代するメソッド
def switch_player(self):
self.current_player = 'white' if self.current_player == 'black' else 'black'
print(f"Now it's {self.current_player.capitalize()}'s turn.")
# 最終スコアを計算するメソッド(簡易的な実装)
def calculate_final_score(self):
stone_counts = self.board.stone_counts()
print(f"Final Score:")
print(f"Black stones: {stone_counts['black']}")
print(f"White stones: {stone_counts['white']}")
if stone_counts['black'] > stone_counts['white']:
print("Black wins!")
elif stone_counts['white'] > stone_counts['black']:
print("White wins!")
else:
print("It's a tie!")
# 碁盤の状態を表示するメソッド
def display_board(self, board):
for x in range(1, board.n + 1):
row = []
for y in range(1, board.m + 1):
stone = board.get_stone(x, y)
if stone is None:
row.append('.')
else:
emotion = stone.get_emotion()
color = stone.get_color()
if emotion == Stone.EMOTION_VALUES['dead']:
symbol = 'X' if color == Stone.COLOR_VALUES['black'] else 'x'
elif emotion == Stone.EMOTION_VALUES['defensive']:
symbol = 'D' if color == Stone.COLOR_VALUES['black'] else 'd'
elif emotion == Stone.EMOTION_VALUES['offensive']:
symbol = 'O' if color == Stone.COLOR_VALUES['black'] else 'o'
else: # normal
symbol = '○' if color == Stone.COLOR_VALUES['black'] else '●'
row.append(symbol)
print(' '.join(row))
print() # 改行
# ゲームを開始
if __name__ == "__main__":
game = Emogo(5, 5)
game.start_game()
# スクリプトを終了しないように待機
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Game terminated.")
碁盤モジュール
board.ino
// ----- 8< ----- 8< ----- 8< ----- 8< ----- 8<
// 情碁 - 碁盤
// ----- 8< ----- 8< ----- 8< ----- 8< ----- 8<
#include <mcp_can.h>
#include <SPI.h>
#include <Wire.h>
//#include "unique-id.h"
//#include "emogo.h"
#define UNIQID 0x22
#define CAN_MASK 0x07FF0000
#define CAN_ORDER (0x0400 + UNIQID)
#define CAN_BLINK 0x01FF
#define CAN_NOTIFY (0x0600 + UNIQID)
#define NOTIFY_ATTACH 0x1
#define NOTIFY_DETACH 0x0
#define NOTIFY_TOUCH 0x2
#define CAN_CS D1
#define CAN_INT D0
#define STONE_I2C_ADDR 0x8
#define STONE_INT D3
#define STONE_ATTACH D2
MCP_CAN CAN0( CAN_CS);
bool can_interrupted = false;
bool stone_touch_interrupted = false;
bool stone_attach_interrupted = false;
uint32_t touch_timer = 0;
uint32_t attach_timer = 0;
bool stone_attached = false;
HardwareSerial *uart;
void can_interrupt( void) {
can_interrupted = true;
}
void stone_touch_interrupt( void) {
if ( ! stone_attached) return;
if ( touch_timer < millis()) {
stone_touch_interrupted = true;
touch_timer = millis() + 500;
}
}
void stone_attach_interrupt( void) {
stone_attach_interrupted = true;
}
uint8_t i2cSend( uint8_t addr, uint8_t* buf, uint32_t len) {
Wire.beginTransmission( addr);
for ( uint32_t i = 0; i < len; i++) {
Wire.write( buf[ i]);
}
return Wire.endTransmission();
}
void setup() {
uart = &Serial;
//uart = &Serial1;
uart->begin( 9600);
delay( 5000);
uart->println( "Starting Emo Go Board module.");
pinMode( CAN_INT, INPUT_PULLUP); // Setting pin xx for ^INT input from CAN
pinMode( STONE_INT, INPUT_PULLUP); // Setting pin xx fo ^INT input from STONE
pinMode( STONE_ATTACH, INPUT_PULLUP);
attachInterrupt( CAN_INT, can_interrupt, FALLING);
attachInterrupt( STONE_INT, stone_touch_interrupt, FALLING);
attachInterrupt( STONE_ATTACH, stone_attach_interrupt, CHANGE);
if ( CAN0.begin( MCP_STD, CAN_500KBPS, MCP_20MHZ) == CAN_OK) uart->println( "MCP2515 initialized.");
else uart->println( "MCP2515 init failed.");
CAN0.init_Mask( 0, 0, CAN_MASK);
CAN0.init_Filt( 0, 0, CAN_ORDER * 0x10000); // システムからの指示、感情変更、点滅状態
CAN0.init_Filt( 1, 0, CAN_BLINK * 0x10000); // 点滅指示
CAN0.init_Mask( 1, 0, CAN_MASK);
CAN0.init_Filt( 2, 0, 0x00000000);
CAN0.init_Filt( 3, 0, 0x00000000);
CAN0.init_Filt( 4, 0, 0x00000000);
CAN0.init_Filt( 5, 0, 0x00000000);
CAN0.init_Filt( 6, 0, 0x00000000);
uart->println( "Done to set filters on MCP2515.");
CAN0.setMode( MCP_NORMAL);
Wire.begin();
//pinMode( SDA, INPUT);
//pinMode( SCL, INPUT);
uart->println( "I2C initialized.");
}
void loop() {
uint32_t canId;
uint8_t len = 0;
uint8_t canBuf[ 8];
uint8_t i2cBuf[ 3];
uint8_t res;
if ( can_interrupted) { // システムからのアクション・CANからメッセージが来たら
can_interrupted = false;
res = CAN0.readMsgBuf( &canId, &len, canBuf);
uart->print( String( canId, HEX));
for ( uint32_t i = 0; i < len; i++) {
i2cBuf[ i] = canBuf[ i];
uart->print( ", " + String( canBuf[ i], HEX));
}
uart->println();
i2cSend( STONE_I2C_ADDR, i2cBuf, len);
}
// 碁石が装着・脱着されたら
if ( stone_attach_interrupted) {
stone_attach_interrupted = false;
attach_timer = millis() + 500;
}
// 碁石の装着・脱着を検知してから500ms後に装着・脱着を判定
if ( attach_timer != 0 && attach_timer < millis()) {
attach_timer = 0;
if ( ! stone_attached && digitalRead( STONE_ATTACH) == LOW) {
stone_attached = true;
canBuf[ 0] = NOTIFY_ATTACH;
res = CAN0.sendMsgBuf( CAN_NOTIFY, 0, 1, canBuf);
uart->println( "Attach");
} if ( stone_attached && digitalRead( STONE_ATTACH) == HIGH) {
stone_attached = false;
canBuf[ 0] = NOTIFY_DETACH;
res = CAN0.sendMsgBuf( CAN_NOTIFY, 0, 1, canBuf);
uart->println( "Detach");
}
}
// 碁石がタッチされたら
if ( stone_touch_interrupted) {
stone_touch_interrupted = false;
if ( stone_attached) {
canBuf[ 0] = NOTIFY_TOUCH;
res = CAN0.sendMsgBuf( CAN_NOTIFY, 0, 1, canBuf);
uart->println( "Touch");
}
}
//delay( 10);
}
碁石
stone.ino
#include <Wire.h>
#include "xiao_round_display.hpp"
#include "data/dead_blink.h"
#include "data/dead_black.h"
#include "data/dead_white.h"
#include "data/defensive_blink.h"
#include "data/defensive_black.h"
#include "data/defensive_white.h"
#include "data/normal_blink.h"
#include "data/normal_black.h"
#include "data/normal_white.h"
#include "data/offensive_blink.h"
#include "data/offensive_black.h"
#include "data/offensive_white.h"
const uint8_t *image[][4] = {
{
dead_blink_png, defensive_blink_png, normal_blink_png, offensive_blink_png,
}, {
dead_black_png, defensive_black_png, normal_black_png, offensive_black_png,
}, {
dead_white_png, defensive_white_png, normal_white_png, offensive_white_png,
}
};
const uint32_t image_len[][4] = {
{
dead_blink_png_len, defensive_blink_png_len, normal_blink_png_len, offensive_blink_png_len,
}, {
dead_black_png_len, defensive_black_png_len, normal_black_png_len, offensive_black_png_len,
}, {
dead_white_png_len, defensive_white_png_len, normal_white_png_len, offensive_white_png_len,
}
};
XiaoRoundDisplay display;
LGFX_Sprite sprite_main( &display), sprite_blink( &display);
// 碁石の色
#define STONE_BLINK 0
#define STONE_BLACK 1
#define STONE_WHITE 2
// 碁石の表情
#define STONE_DEAD 0
#define STONE_DEFENSIVE 1
#define STONE_NORMAL 2
#define STONE_OFFENSIVE 3
// 碁石の向き
#define STONE_NORTH 0
#define STONE_EAST 90
#define STONE_SOUTH 180
#define STONE_WEST 270
// 碁石の状態
uint32_t stone_color = STONE_BLACK;
uint32_t stone_emotion = STONE_NORMAL;
uint32_t stone_direction = STONE_NORTH;
bool stone_blink_state = false;
bool stone_blink_expression = false;
// 外部通信定義
#define STONE_PWR D0
#define STONE_INT D2
#define TOUCH_INT D7
#define I2C_ADDR 0x8
uint8_t i2c_rxbuf[ 5] = { 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t i2c_txbuf[ 2] = { 0x00, 0x00};
bool i2c_recv_interrupted = false;
bool i2c_send_interrupted = false;
bool touch_interrupted = false;
uint32_t touch_timer = 0;
#define SET_STATE 1
#define SET_BLINK 2
#define BLINK 3
HardwareSerial *uart;
void receiveEvent( int num) {
i2c_recv_interrupted = true;
uart->println( "enter receiveEvent function.");
for ( uint32_t i = 0; Wire.available(); i++) {
if ( i < 5) i2c_rxbuf[ i] = Wire.read();
else Wire.read();
}
}
void sendEvent() {
i2c_send_interrupted = true;
for ( uint32_t i = 0; i < 2; i++) {
Wire.write( i2c_txbuf[ i]);
}
}
void touchEvent() {
if ( touch_timer < millis()) {
digitalWrite( STONE_INT, LOW);
uart->println( "Touch Event " + String(millis()));
touch_timer = millis() + 100;
} else {
digitalWrite( STONE_INT, HIGH);
}
}
void setup() {
uart = &Serial;
//uart = &Serial1;
uart->begin( 9600);
delay( 1000);
uart->println( "start:");
// I2C settings
Wire.begin( I2C_ADDR);
//pinMode( SDA, INPUT);
//pinMode( SCL, INPUT);
Wire.onReceive( receiveEvent);
Wire.onRequest( sendEvent);
uart->println( "I2C settings done.");
// Notify power and touch
pinMode( STONE_PWR, OUTPUT);
digitalWrite( STONE_PWR, LOW);
pinMode( STONE_INT, OUTPUT);
digitalWrite( STONE_INT, HIGH);
pinMode( TOUCH_INT, INPUT_PULLUP);
attachInterrupt( TOUCH_INT, touchEvent, FALLING);
// Display settings
display.init();
sprite_main.createSprite( 120, 120);
sprite_blink.createSprite( 120, 120);
//sprite_main.drawPng( image[ stone_color][ stone_emotion], image_len[ stone_color][ stone_emotion], 0, 0);
//sprite_blink.drawPng( image[ STONE_BLINK][ stone_emotion], image_len[ STONE_BLINK][ stone_emotion], 0, 0);
display.startWrite();
display.clear();
//sprite_main.pushRotateZoom( 0, 2, 2);
display.endWrite();
}
void loop() {
String dbgMsg;
if ( i2c_recv_interrupted) {
i2c_recv_interrupted = false;
dbgMsg = "command, parameters = " + String( i2c_rxbuf[ 0], HEX);
for ( uint32_t i = 1; i <= 4; i++) {
dbgMsg += ", " + String( i2c_rxbuf[ i], HEX);
}
uart->println( dbgMsg);
switch ( i2c_rxbuf[ 0]) {
case SET_STATE:
stone_color = i2c_rxbuf[ 1];
stone_emotion = i2c_rxbuf[ 2];
stone_direction = (uint32_t)i2c_rxbuf[ 3] * 256 + i2c_rxbuf[ 4];
uart->println( String(stone_color)+" "+String(stone_emotion)+" "+String(stone_direction));
break;
case SET_BLINK:
stone_blink_state = ( i2c_rxbuf[ 1] != 0x00) ? true : false;
stone_blink_expression = false;
break;
case BLINK:
if ( stone_blink_state) stone_blink_expression = ( i2c_rxbuf[ 1] != 0x00) ? true : false;
else stone_blink_expression = false;
break;
}
if ( stone_blink_state) {
display.startWrite();
if ( stone_blink_expression) sprite_blink.pushRotateZoom( stone_direction, 2.0, 2.0);
else sprite_main.pushRotateZoom( stone_direction, 2.0, 2.0);
display.endWrite();
} else {
sprite_main.drawPng( image[ stone_color][ stone_emotion], image_len[ stone_color][ stone_emotion], 0, 0);
sprite_blink.drawPng( image[ STONE_BLINK][ stone_emotion], image_len[ STONE_BLINK][ stone_emotion], 0, 0);
display.startWrite();
sprite_main.pushRotateZoom( stone_direction, 2.0, 2.0);
display.endWrite();
}
}
if ( touch_interrupted) {
touch_interrupted = false;
}
}
xiao_round_display.hpp
#ifndef XIAO_ROUND_DISPLAY
#define XIAO_ROUND_DISPLAY
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
class Touch_XiaoRound : public lgfx::v1::ITouch {
public:
Touch_XiaoRound();
bool init() override;
void wakeup() override;
void sleep() override;
uint_fast8_t getTouchRaw(lgfx::v1::touch_point_t *tp, uint_fast8_t count) override;
};
class XiaoRoundDisplay : public lgfx::LGFX_Device {
lgfx::Panel_GC9A01 _panel;
lgfx::Bus_SPI _bus;
lgfx::Light_PWM _light;
Touch_XiaoRound _touch;
public:
XiaoRoundDisplay();
};
#endif //#ifndef XIAO_ROUND_DISPLAY
xiao_round_display.ino
#include "xiao_round_display.hpp"
Touch_XiaoRound::Touch_XiaoRound() {
_cfg.x_min = _cfg.y_min = 0;
_cfg.x_max = _cfg.y_max = 239;
_cfg.i2c_addr = 0x2e;
}
bool Touch_XiaoRound::init() {
if (isSPI()) {
return false;
}
if (_cfg.pin_int >= 0) {
lgfx::pinMode(_cfg.pin_int, lgfx::v1::pin_mode_t::input_pullup);
}
return lgfx::i2c::init(_cfg.i2c_port, _cfg.pin_sda, _cfg.pin_scl).has_value();
}
void Touch_XiaoRound::wakeup() {}
void Touch_XiaoRound::sleep() {}
uint_fast8_t Touch_XiaoRound::getTouchRaw(lgfx::v1::touch_point_t *tp, uint_fast8_t count) {
tp[0].size = 0;
tp[0].id = 0;
if (_cfg.pin_int < 0) {
return 0;
}
//FIXME:
if ((bool)lgfx::gpio_in(_cfg.pin_int)) {
::delay(10);
if ((bool)lgfx::gpio_in(_cfg.pin_int)) {
return 0;
}
}
uint8_t buf[5];
if (!lgfx::i2c::transactionRead(_cfg.i2c_port, _cfg.i2c_addr, buf, 5, _cfg.freq).has_value()) {
return 0;
}
if (buf[0] != 1) {
return 0;
}
tp[0].x = buf[2];
tp[0].y = buf[4];
tp[0].size = 1;
return 1;
}
XiaoRoundDisplay::XiaoRoundDisplay() {
auto bus_cfg = _bus.config();
bus_cfg.spi_host = 0; // for XIAO RP2040
bus_cfg.spi_mode = 0;
bus_cfg.freq_write = 80000000;
bus_cfg.freq_read = 20000000;
bus_cfg.pin_sclk = D8; // for XIAO RP2040
bus_cfg.pin_mosi = D10; // for XIAO RP2040
bus_cfg.pin_miso = D9; // for XIAO RP2040
bus_cfg.pin_dc = D3; // for XIAO RP2040
_bus.config(bus_cfg);
_panel.setBus(&_bus);
auto panel_cfg = _panel.config();
panel_cfg.pin_cs = D1; // for emo-go on XIAO RP2040
panel_cfg.pin_rst = -1;
panel_cfg.pin_busy = -1;
panel_cfg.memory_width = 240;
panel_cfg.memory_height = 240;
panel_cfg.panel_width = 240;
panel_cfg.panel_height = 240;
panel_cfg.offset_x = 0;
panel_cfg.offset_y = 0;
panel_cfg.offset_rotation = 0;
panel_cfg.dummy_read_pixel = 8;
panel_cfg.dummy_read_bits = 1;
panel_cfg.readable = false;
panel_cfg.invert = true;
panel_cfg.rgb_order = false;
panel_cfg.dlen_16bit = false;
panel_cfg.bus_shared = true;
_panel.config(panel_cfg);
auto light_cfg = _light.config();
// light_cfg.pin_bl = D6; // for XIAO RP2040
light_cfg.pin_bl = -1; // for emo-go on XIAO RP2040
light_cfg.invert = false;
light_cfg.freq = 44100;
light_cfg.pwm_channel = 7;
_light.config(light_cfg);
_panel.setLight(&_light);
/* Don't use the feature that tells where it touched
* because emogo stone should be i2c slave but it foced to be i2c master.
auto touch_cfg = _touch.config();
touch_cfg.pin_int = D7; // for XIAO RP2040
touch_cfg.i2c_port = 1; // for XIAO RP2040
touch_cfg.pin_sda = D4; // for XIAO RP2040
touch_cfg.pin_scl = D5; // for XIAO RP2040
touch_cfg.freq = 400000;
_touch.config(touch_cfg);
_panel.setTouch(&_touch);
*/
setPanel(&_panel);
}
碁石の表情データを書き込んだヘッダファイルは省略しました。また、Arduino IDEの各種設定、Raspberry PiのCAN拡張HATの設定なども省略しました。それぞれ以下のサイトを参考にしてください。
-
mocketech
さんが
2024/10/29
に
編集
をしました。
(メッセージ: 初版)
-
mocketech
さんが
2024/10/29
に
編集
をしました。
-
mocketech
さんが
2024/10/29
に
編集
をしました。
-
mocketech
さんが
2024/10/29
に
編集
をしました。
-
mocketech
さんが
2024/10/29
に
編集
をしました。
Opening
mocketech
2024/12/11
ログインしてコメントを投稿する回路図(KiCad)、ソースコード、3Dデータ(FreeCAD, libreOffice)のデータをgithubに公開しました。
https://github.com/mocketech/EmoGo