LoginSignup
6

More than 3 years have passed since last update.

posted at

updated at

「"声"で打つオセロ」の作り方 〜〜オリジナル Google Home づくりを目指して Ep 1. 〜〜

本記事では、Raspberry Piを用いた「"声"で打つオセロ」の作り方を紹介します。

言うまでもなくオセロや囲碁、将棋といったボードゲームは、石や駒を使って盤の上で対戦するゲームですが、「"声"で打つオセロ」とは、その名の通り石に触れることなく、声だけで対戦ができるオセロです。

作るきっかけとしては、以前から興味のあった電子工作ともともと勉強してきた人工知能をかけ合わせたい!という気持ちからいろいろと考え、電子工作×音声認識を活かせるオセロを思いつきました。(囲碁や将棋などにも応用可能ですが、今回は簡単なオセロで)

将来的に「"声"で指す将棋」を作って、羽生さんにプレイしてもらうという夢をひそかに持っています。

さらに「"声"で打つオセロ」を作る中で、音声認識や自然応答のアルゴリズムの実装、またクエリに対する検索結果の返却など、機能をリッチにしていくことで、昨年発売され話題になったGoogle Homeもどきが作れるのではないかと考えました。
なので、今回はオセロのプレイに特化していますが、最終的にはGoogle Homeが持つような機能を実装していく予定です。

完成品

まずは、完成品を見てみます。

qvQqQ5b019e26cb81bbc1607b2e34.jpg

配線がとんでもないことになっていますね。
画面中央に見えるのが、$8 \times 8$のオセロの盤面をLEDで表現しているもので、左下のマイクから座標を声で入力することで、対応する座標が点灯し、挟まれているLEDが反転します。(例えば、「F5赤」と発声すれば、左上から数えて5行6列のLEDが赤色に変わります。)

デモ動画も公開しているので、ぜひご覧ください。

準備する部品

「"声"で打つオセロ」に必要となる部品は以下となります。

ディスプレイの有無などにも依りますが、おおよそ2~4万程度で揃えることができます。
一応、例として型番も示しておきましたが、特にこれでないとダメということはないのでお好みで。

(設計が固まるまでに何度も買い直したりしたので、必要なものが固まるまでかなりつらかったです...。)

グループ 名称 型番 個数
Raspberry Pi 本体関連 Raspberry Pi Raspberry Pi 3 Model B 1
電源アダプタ 1
micro SD 32GB 1
Raspberry Pi ケース(任意) 1
Raspberry Pi 周辺機器 ディスプレイ 1
HDMIケーブル 1
USBマウス 1
USBキーボード 1
回路関連 ミニブレッドボード BB-601 16
ブレッドボード EIC-801 6
赤色LED OSDR5113A 64
青色LED OSUB5111A-ST(15度)10cd 64
抵抗 カーボン抵抗(炭素皮膜抵抗) 1/4W 150Ω 128
ジャンパーワイヤー 大量
シフトレジスタ SN74HC595N 16
丸ピンICソケット(任意) 16

今回は、オセロの白黒を赤青LEDで表現していますが、もちろん別の色でも可能です。
後から気付いたのですが、フルカラーLEDを用いれば後で示す回路が少し単純化できるかもしれません。(作る方はぜひ試してみてください)

また、はんだ付けが出来る方はブレッドボードやジャンプワイヤーなど節約することが出来るので、より綺麗により安価に回路を作れると思います。

Raspberry Pi とは

実際の作り方に入る前に、簡単に今回の作品の鍵となるRaspberry Pi(以下、ラズパイ)について見ていきましょう。

実物は以下のようなものです。

GCjEG5b002730edef02bc601fb3fe.jpg

名刺と同じくらいで、小さいですね。
イメージとしては、2000年代の性能のパソコンが、とても小さくなったもので良いと思います。

この手軽さから、ラズパイは電子工作でよく用いられます。
他に有名なものとしては、Arduinoも挙げられますね。どちらも特徴があるので、気になる方は調べて見てください。

ラズパイには、GPIO(General Purpose Input/Output)ポートという様々な出力を行うピンがあります。([1]より)

Raspberry-Pi-GPIO-Layout-Model-B-Plus-rotated-2700x900-1024x341.png

例えば、1番のピンは3.3Vの電源、6番のピンはグラウンドとなるので、1番のピンと6番のピンの間にLEDを繋ぐことで、LEDを光らせることができます。

覚える必要はありませんが、後で使用することになるので、図は参照できるようにしておきましょう。

手順

実際に作り方を見ていきます。

大まかな流れとしては、以下の3つとなります。

  1. OSのインストール
  2. 回路を組み立てる
  3. コーディングする

それでは、一つ一つ細かく見ていきましょう。

OSのインストール

ラズパイは、そのままで使用することはできません。
まずは、ラズパイを動かすためのOSをインストールする必要があります。

はじめに、microSDに以下の手順でOSをインストールします。

  1. https://www.raspberrypi.org/downloads/ へアクセス
  2. 「NOOBS」のアイコンをクリック
  3. 「Download ZIP」をクリック
  4. ダウンロードされた圧縮ファイルを展開
  5. microSDに展開後のファイルをコピー

スクリーンショット 2018-05-14 9.48.18.png

ここまでできたら、いよいよラズパイ本体を動かす準備ができました。
以下の手順で、ラズパイ本体にOSをインストールします。

  1. microSDをラズパイにセット
  2. マイク、キーボード、ディスプレイを接続
  3. マイクロUSBの電源を接続
  4. 「Raspbian」にチェックを入れ、「Install」ボタンをクリック
  5. 起動後、左上の「Menu」から「Preferences」→「Raspberry Pi Configuration」で言語、タイムゾーン、キーボードなど各種設定

細かい設定などは、こちらの本「カラー図解 最新 Raspberry Piで学ぶ電子工作 作って動かしてしくみがわかる」を参照しました。

回路を組み立てる

今回、物理的な動作としては、LEDを光らせるだけなので、それほど難しい回路は必要ありません。
ただし、盤面の状況に応じて、赤色/青色LEDを切り替える必要があります。
そこで、今回はLEDを制御するためにシフトレジスタを使用します。

シフトレジスタ(74HC595)

今回はシフトレジスタとして、74HC595を用います。
74HC595は8ビットシフトレジスタなので、8個の出力を制御することができます。(光らせたり、光らせなかったり)

74HC595は以下の図のように16個のピンを持っています。
各ピンの配置は以下の通りとなります。([2]より)

スクリーンショット 2018-05-04 13.09.44.png

各ピンの役割は次の通りです。

ピン名称 役割
QA~QH 出力先(8個)
GND グラウンド
QH' 次のシフトレジスタへの接続
SCLR リセットクロック
SCK シフトレジスタクロック
RCK ラッチクロック
G HiにするとQA~QHがハイインピーダンス
SI シリアルデータ入力
VCC 電源

シフトレジスタの接続先は、以下のように接続します。(参考: 少ない出力ピンで、大量のLEDを制御する(シフトレジスタ使用))

ピン番号 ピン名称 接続先(一つ目) 接続先(二つ目以降) 接続先(最後)
1 QB 2つ目のLEDのアノード 2つ目のLEDのアノード 2つ目のLEDのアノード
2 QC 3つ目のLEDのアノード 3つ目のLEDのアノード 3つ目のLEDのアノード
3 QD 4つ目のLEDのアノード 4つ目のLEDのアノード 4つ目のLEDのアノード
4 QE 5つ目のLEDのアノード 5つ目のLEDのアノード 5つ目のLEDのアノード
5 QF 6つ目のLEDのアノード 6つ目のLEDのアノード 6つ目のLEDのアノード
6 QG 7つ目のLEDのアノード 7つ目のLEDのアノード 7つ目のLEDのアノード
7 QH 8つ目のLEDのアノード 8つ目のLEDのアノード 8つ目のLEDのアノード
8 GND GND GND GND
9 QH' 次のシフトレジスタのSI 次のシフトレジスタのSI なし
10 SCLR 3.3V 3.3V 3.3V
11 SCK GPIO 19
次のシフトレジスタのSCK
次のシフトレジスタのSCK 前のシフトレジスタのSCK
12 RCK GPIO 20
次のシフトレジスタのRCK
次のシフトレジスタのRCK 前のシフトレジスタのRCK
13 G GND GND GND
14 SI GPIO 21 前のシフトレジスタのQH' 前のシフトレジスタのQH'
15 QA 1つ目のLEDのアノード 1つ目のLEDのアノード 1つ目のLEDのアノード
16 VCC 3.3V 3.3V 3.3V

複雑に見えるかもしれませんが、ピンを一つ一つ見ていけば、それほど難しくないと思います。

完成した回路

シフトレジスタについて理解できたら、後はLEDと抵抗を繋ぐだけです。
抵抗は150Ωのものを用いましたが、使用するLEDとオームの法則から、適当な抵抗を使用してください。

完成した回路は以下となります。(再掲)

qvQqQ5b019e26cb81bbc1607b2e34.jpg

はんだ付けを嫌ってこのような設計にしてしまいましたが、素直にはんだ付けすれば良かったです。

コーディングする

コーディングでポイントとなるのは以下の三点です。

  • シフトレジスタを用いたLEDの制御
  • 音声認識
  • オセロの処理

一つ一つ見ていきましょう。

シフトレジスタを用いたLEDの制御

シフトレジスタを組み込んだ回路を設計しただけでは、自由にLEDを制御することができません。
(参考: RaspberryPiにシフトレジスタ【74HC595】 を使って、複数のLEDをチカチカさせてパーリィーナイしてみた【Python使用】)

実際にLEDを制御するためには、各ピンにシリアルデータを伝えなければなりません。
今回は$8 \times 8$の盤面に3つの状態(0なら消灯、1なら赤LED点灯、2なら青LED点灯)を情報として保持させます。
この盤面からLEDを点灯させるためのコードが以下となります。

#!/usr/local/bin/python
# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
import time

# 各GPIOの番号
SCK = 19
RCK = 20
SI = 21

# GPIOポートを使用
GPIO.setmode(GPIO.BCM)

# 初期化
GPIO.setup(SCK, GPIO.OUT)
GPIO.setup(RCK, GPIO.OUT)
GPIO.setup(SI, GPIO.OUT)

def reset():
    """
    盤面をリセットする
    """
    GPIO.output(SCK, GPIO.LOW)
    GPIO.output(RCK, GPIO.LOW)
    GPIO.output(SI, GPIO.LOW)

def shift(PIN_NUM):
    """
    受け取ったピン番号をシフトする

    @param ピン番号
    """
    GPIO.output(PIN_NUM, GPIO.HIGH)
    GPIO.output(PIN_NUM, GPIO.LOW)

def all_high():
    """
    すべてのLEDを点灯させる(動作確認用)
    """
    reset()

    for j in range(16):
        for i in range(8):
            GPIO.output(SI, GPIO.HIGH)
            shift(SCK)
        shift(RCK)

def board_flash(board):
    """
    受け取った盤面の状態をLEDに反映させる

    @param 盤面の状態
    """
    #reset()
    BOARD_SIZE = 8

    for i in range(BOARD_SIZE):
        for j in range(BOARD_SIZE):
            if board[abs(7-i)][abs(7-j)] == 0: # 消灯状態
                GPIO.output(SI, GPIO.LOW)
                shift(SCK)
                GPIO.output(SI, GPIO.LOW)
                shift(SCK)
            elif board[abs(7-i)][abs(7-j)] == 1: # 赤LED
                GPIO.output(SI, GPIO.LOW)
                shift(SCK)
                GPIO.output(SI, GPIO.HIGH)
                shift(SCK)
            elif board[abs(7-i)][abs(7-j)] == 2: # 青LED
                GPIO.output(SI, GPIO.HIGH)
                shift(SCK)
                GPIO.output(SI, GPIO.LOW)
                shift(SCK)

            if j == 3 or j == 7: # 8個出力するたびにラッチクロックをシフト
                shift(RCK)

音声認識

次に声を認識する手順を紹介します。
ラズパイで音声認識を行うもっとも有名な方法は、汎用大語彙連続音声認識エンジンJuliusを用いる方法です。

Juliusのダウンロードからデモの実行は、こちらのサイト「Julius の使い方(version 4.4.2 対応版) | Feijoa.jp」が詳しいです。
設定ファイルはこちらのサイト「ラズパイに、人の声を理解させる。」に従いました。

音声認識の精度を高める

ここまででJuliusによる音声認識ができたと思いますが、そのままだと認識精度があまりよくありません。
そこで、Juliusの記述文法を用いて、特定のルールを覚えさせることにより認識精度を高めます。(参考: Julius の記述文法を用いて音声認識精度をあげてみた - 凹みTips第7章 言語モデル)

  • grammarファイル

grammarファイルは構文の制約を記述することができます。
すなわち、どのような文章の言葉が、入力として与えられるかを定義します。

オセロでは「A1」のように、一つ目の単語として「アルファベット」、二つ目の単語として「数字」が入力として与えられます。今回は、それにわかりやすくするためにLEDの「色」を追加するので、grammarファイルは以下のように書きます。

voice.grammar

S          :  NS_B ALPHABET_N NUMBER_N COLOR_N NS_E
# アルファベット
ALPHABET_N :  ALPHABET
# 数字
NUMBER_N   :  NUMBER
# 色
COLOR_N    :  COLOR

NS_B、NS_Eはそれぞれ無音区間をあらわします。
これで文法が定義できました。

  • vocaファイル

vocaファイルは、単語表記と単語の読みを定義することができます。

オセロでは、アルファベットは「A」から「H」までの8個あれば十分なので、それ以外のアルファベットは定義する必要がありません。
設定ファイルを見るとわかりやすいと思います。

voice.voca

% ALPHABET
A         e i
B         b i:
C         s i:
D         d e:
E         i:
F         e h u
G         g i:
H         e i ch i
% NUMBER
1         i ch i
2         n i:
3         s a N
4         y o N
5         g o:
6         r o k u
7         n a n a
8         h a ch i
% COLOR
赤        a k a
青        a o
% NS_B
<s>            silB
% NS_E
</s>           silE

これで単語が定義できました。

後は、参考にあるようにこれらのファイルをコンパイルし、実行時に指定することで認識精度が上がります。
自分の手元では以下のように実行しています。

# grammar/vocaファイルをコンパイル
mkdfa.pl voice

# 実行
julius -C conf/vo_main.jconf -gram grammar/voice

モジュールモード

Juliusをモジュールモードで実行することで、音声認識サーバとして使用し、TCP/IP 経由でクライアントと接続することで、認識した音声を用いて実際の処理を書くことができます。(参考: ラズパイで音声認識つかってLチカしてみるただいまシステムの中身② Socketを介した認識結果のXMLパース)

julius -C conf/vo_main.jconf -gram grammar/voice -gram grammar/settings -module &

この状態で以下のコードを実行することで、マイクから入力された音声により、異なる処理を実行することができます。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import string
import copy
import othello
import flash

host = 'localhost'
port = 10500

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))

# 盤面の初期化
prev_board = othello.zero_board()
curr_board = othello.zero_board()

# マイク入力文字列の初期化
data = ""

def split_string(string):
    """
    入力された3文字を分解

    @param string :マイクから入力された文字列
    @return マイクから入力された列
    @return マイクから入力された行
    @return マイクから入力された色
    """
    string_uni = string.decode('utf-8')
    chars = list(unicode(string_uni))
    chars = [char.encode('utf-8') for char in chars]

    column = chars[0]
    row = chars[1]
    color = chars[2]

    return column, row, color

def convert_to_coodinate(column, row, color):
    """
    入力された列、行を配列のインデックスに変換

    @param  column :マイクから入力された列
    @param  row    :マイクから入力された行
    @param  color  :マイクから入力された色
    @return 配列の列インデックス
    @return 配列の行インデックス
    @return 色
    """
    COLUMN_LIST = ["A", "B", "C", "D", "E", "F", "G", "H"]
    COLOR_LIST = ["赤", "青"]

    column_tmp = COLUMN_LIST.index(column)
    row_tmp = int(row) - 1
    if color == "赤":
        color_tmp = "RED"
    elif color == "青":
        color_tmp = "BLUE"

    return column_tmp, row_tmp, color_tmp


try:
    while True:
        # マイクから入力された文字列の読み込み
        while (string.find(data, "\n.") == -1):
            data = data + client.recv(1024)

        strTemp = ""

        for line in data.split('\n'):
            index = line.find('WORD="')

            if index != -1:
                line = line[index + 6:line.find('"', index + 6)]
                if line != "<s>" and line != "</s>": 
                    strTemp = strTemp + line

        print(strTemp)
        if strTemp == "START":
            print("ボード初期化")
            prev_board = curr_board
            curr_board = othello.initialize_board()
            flash.board_flash(curr_board)
            print(curr_board)
        elif strTemp == "BACK":
            print("一つ戻る")
            tmp_board = copy.copy(curr_board)
            curr_board = prev_board
            flash.board_flash(curr_board)
            print(curr_board)
        elif strTemp == "END":
            print("終了")
            tmp_board = copy.copy(curr_board)
            curr_board = prev_board
            print(curr_board)
        elif strTemp != "":
            # 文字列のsplit
            input_column, input_row, input_color = split_string(strTemp)
            column, row, color = convert_to_coodinate(
                input_column, input_row, input_color)

            # オセロの処理
            new_board, change_flag = othello.next_board(column, row, color, curr_board)

            # 盤面の更新
            if change_flag == True:
                print("更新")
                prev_board = curr_board
                curr_board = new_board
            else:
                curr_board = new_board

            print(curr_board)

            # LEDの制御
            flash.board_flash(curr_board)

        data = ""

except KeyboardInterrupt:
    print("KeyboardInterrupt")
    pass

オセロの処理

最後にオセロの処理を書きます。

オセロの処理は、特別なことはしていないのでこちらのサイト「「プログラミング初心者がpythonの勉強がてら、オセロAIを作ってみた」をリファクタリングさせていただいた」を参考に書きました。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import numpy as np
import copy

def zero_board():
    """
    消灯状態の盤面を返す

    @return 8*8のnp.array
    """
    BOARD_SIZE = 8

    return np.zeros((BOARD_SIZE, BOARD_SIZE))

def initialize_board():
    """
    初期化された盤面を返す

    @return 8*8のnp.array
    """
    BOARD_SIZE = 8
    INITIAL_RED   = [(4, 3), (3, 4)] 
    INITIAL_BLUE = [(3, 3), (4, 4)] 

    tmp_board = np.zeros((BOARD_SIZE, BOARD_SIZE))

    for coo in INITIAL_RED:
        tmp_board[coo] = 1

    for coo in INITIAL_BLUE:
        tmp_board[coo] = 2

    return tmp_board

def next_board(column, row, color, board):
    """
    打つ手から次の盤面を返す

    @param column   : int型の列番号
    @param row      : int型の行番号
    @param color    : str型の色
    @param board    : 8*8のnp.array型の現在の盤面
    @return 8*8のnp.array
    @return 正しい置石であったかどうかのboolean
    """
    BOARD_SIZE = 8
    DIRECTIONS_XY = ((-1, -1), (+0, -1), (+1, -1),
                     (-1, +0),           (+1, +0),
                     (-1, +1), (+0, +1), (+1, +1))

    tmp_board = copy.copy(board)
    reverse_flag = False

    # 既に石が置かれている場合無効
    if tmp_board[row][column] != 0:
        print("既に石が置かれています。")
        return board, False

    # 石を置く
    if color == "RED":
        stone = 1
    elif color == "BLUE":
        stone = 2

    tmp_board[row][column] = stone

    # 反転
    for dx, dy in DIRECTIONS_XY:
        x = row
        y = column
        target = 0
        for n in range(BOARD_SIZE):
            x += dx
            y += dy
            if not (0 <= x < BOARD_SIZE and 0 <= y < BOARD_SIZE): # 盤外に出る場合
                target = 0
                break
            if tmp_board[x][y] == 0: # 石が置いていない場合
                target = 0
                break
            if tmp_board[x][y] == stone: # 新しく置いた石と同じ色の石が置いている場合
                target =  n
                break

        for i in range(1, target + 1):
            reverse_flag = True
            tmp_board[row + i * dx][column + i * dy] = stone

    # 一つも反転していない場合無効
    if reverse_flag == False:
        print("石の座標、もしくは色が適切ではありません。")
        return board, False

    return tmp_board, True

これらを実行することで、「"声"で打つオセロ」が動きます!

オプション:効果音、BGMの付与

ここまでで、オセロとしての機能は完成しましたが、このままだと少し寂しいので効果音やBGMをつけます。

今回はmp3ファイルを再生することのできるpygameを用いました。デフォルトでインストールされているライブラリなので、簡単に使用することができます。

ソースコードとしては、以下のように実装します。(参考:「Raspberry Piで音楽(wav/mp3)ファイルを再生する方法 python編」)

#!/usr/bin/env python
#-*- cording: utf-8 -*-

import pygame.mixer
import time

# mixerモジュールの初期化
pygame.mixer.init()
# 音楽ファイルの読み込み
pygame.mixer.music.load("ファイル名.mp3")
# 音楽再生、および再生回数の設定(-1はループ再生)
pygame.mixer.music.play(-1)

time.sleep(60)
# 再生の終了
pygame.mixer.music.stop()

これにより、盤面の更新時やゲームの終了時に適切な効果音やBGMを再生することで、よりゲームっぽく仕上げることができます。

おわりに

長い記事になってしまいましたが、見ていただきありがとうございます。

筆者はGWから電子工作を始め、三週間ぐらいで今回の「"声"で打つオセロ」を作ることができました。(デモ動画の収録関連で記事は遅れてしまいましたが...)
慣れが早い人なら1、2週間で作ることもできると思うので、ぜひ作って遊んでみてください。

今後の課題としては、オセロ以外のゲームへの応用、回路の簡素化、音声認識精度の向上などが挙げられます。
将来的には、一つのボードでたくさんのゲームができるようになるかもしれませんね。

また、タイトルにもある通り、今後は検索機能や自然応答などの機能を追加することで、よりリッチなデバイスの完成を目指します。
期間は開いてしまうと思いますが、よろしくお願いします。

記事に関してお気付きの点、またご感想がありましたら、遠慮なくコメントやTwitterの方でお声がけください。

参考文献

[1] "Simple Guide to the Raspberry Pi GPIO Header and Pins - Raspberry Pi Spy",
2012, Available at https://www.raspberrypi-spy.co.uk/2012/06/simple-guide-to-the-rpi-gpio-header-and-pins/#prettyPhoto.
[2] "TC74HC595AP_07 datasheet(1/11 Pages) TOSHIBA | CMOS Digital Integrated Circuit Silicon Monolithic 8-Bit Shift Register/Latch (3-state)", 2007, Available at http://html.alldatasheet.com/html-pdf/214419/TOSHIBA/TC74HC595AP_07/301/1/TC74HC595AP_07.html.

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6