ACRiルームのFPGAで○×ゲームを作って遊んでみよう
ACRi ルームで楽しく FPGA 開発をはじめてみる題材としてゲームを作ってみるのはいかがでしょう。この連載では、FPGA を使った○×ゲームを作ることを題材に FPGA 開発をはじめてみることにします。リソースは○×ゲームで FPGA 開発をはじめてみようにあります。
今回の記事のねらい
開発の流れは、○×ゲームで FPGA 開発をはじめてみよう (RTL 編) をご覧ください。この記事では、○×ゲームがどう作られているか、を説明します。
はじめに全体の構成を説明した後で、個々のモジュールの動作内容とシミュレーションで動作確認する様子を紹介します。実際のコードと合わせて読んでもらえるとよりよくわかると思いますが、この記事だけでも、どんな風に実装しているか、どんな風に動作を確認しているかの雰囲気はわかってもらえると思います。
なお、記事の内容に沿って、手元でもシミュレータを動かしながら読みすすめたい場合には、○×ゲームで FPGA 開発をはじめてみよう (RTL 編) – シミュレーションまでの手順を辿り、Vivado のプロジェクトを用意してください。
実装の概要
○×ゲームは、次のようなモジュールで構成されています。
ゲームを構成するモジュールの役割はそれぞれ次の通りです。
game_manager_i(game_manger.sv)
ゲームの進行を管理します。このモジュールがゲームの状態を保持します。print_board_i(print_board.sv)
盤面を表示しますrecv_user_input_i(recv_user_input.sv)
ユーザの手を受けとりますmake_turn_i(make_turn.sv)
FPGA プレーヤ側の手を決めますmake_judge_i(make_judge.sv)
ゲームの判定を行ないますprint_result_i(print_result.sv)
ゲームの結果を表示します
game_manager_i
以外のモジュールは、game_manaer_i
から動作開始信号がアサートされることで動作を開始し、動作が終わると ready
信号をアサートするように実装します。
また、UART で PC とデータをやりとりするために次のモジュールが組み込まれています。
uart_tx_i(uart_tx.sv)
UART で PC にデータを送信します(盤面の表示や結果の表示に使います)uart_rx_i(uart_rx.sv)
UART で PC からデータを受信します(プレーヤの入力を受け取るのに使います)
uart_tx_i
を介した文字出力は、出力が必要な複数のモジュールで排他的に利用することにします。
○×ゲームの状態は、○と×が置かれているマスで管理できます。3×3 のマスに相当する 9bit のビット列を使って、○が置かれているかどうかのビット列 (board_a
) と×が置かれているかどうかのビット列 (board_b
) を game_manager_i
に用意しました。
たとえば、board_a
が 000100001
、board_b
が 000000010
は、次のような盤面に相当します。
game_manager_i
以外のモジュールには game_manager_i
の中に定義されているゲームの状態を wire
で接続して、その時々の状態に応じた動作ができるようにします。
ゲームの進行管理
まずは、全体の流れを見てみましょう。この実装では、進行を管理する game_manager_i
が、順序よく他のモジュールを駆動してゲームをすすめます。次の図がゲームの進行をフローチャートにしたものです。なお、実装を簡単にするために、はじめの一手は FPGA が打つことに決め打ちしています。
Vivado を使って、動作をシミュレーションしてみた様子が次の通りです。
game_manager_i
は、kick
が 1
になると動作を開始し、print_board_req
→ make_judge_req
→ make_turn_req
→ print_board_req
→ make_judge_req
→ recv_req
→ … と,順々にモジュールを駆動させるためのフラグを立ててていく様子がみてとれます.
最終的に,make_judge_req
の後に end_of_game
が 1
であれば、結果を表示するモジュールを駆動してゲームを終了する動作が確認できました。
盤面の表示
盤面は print_board_i
によって描画されます。このモジュールは、与えられたゲームの状態 (board_a
と board_b
) を人間が見える形で表現するのが役割りです。盤面に -
、+
と |
を、○と×に o
と x
を使って出力します。ターミナルで正しく表示するためには、各行の終わりに改行コードである CR
と LF
を入れるのを忘れてはいけません。
動作をシミュレーションした様子が次の通りです。
与えられたボードの状態、○に相当する board_a
が 100010001
と×に相当する board_b
が 010101010
、に対して UART に 盤面の区切り記号や o
と x
を出力しています(uart_din
の行を見てください)。SEND_LINE
が一行の表示に対応していて行の終わり毎に改行コードの CR
と LF
を出力しています(表示は空で見えませんが)。
ユーザの手を受け取る
FPGA と人間で○×ゲームを遊べるようにするためには、人間の手を FPGA 内部に取り込む必要があります。ターミナルから列と行をそれぞれ '0'
〜 '2'
の文字で入力させることにします。不正な入力や、既に○か×が置いてある場所を選んだ場合にはエラーを示す error_flag
を立てておきます。
今回の実装では、recv_user_input_i
の動作フローは次の通りです。
動作をシミュレーションした様子が次の通りです。
このシミュレーションでは、黄色のマーカーまでと青のマーカーまでの二回入力を受け付けています。どちらも、uart_q
で '0'
と '1'
がユーザから入力されたというシナリオでの動作です。一回目は正しく入力を受理して board_b_out
が 000001000
に変っていること、二回目は既に着手済のマスを選択していることで error_flag
が 1
になっていることが見て取れます。
FPGA プレーヤの手を決める
FPGA に手を決めさせる必要があります。いろいろと実装方法は考えられますが、今回の make_turn_i
では、ルールに則った手を打つシンプルな実装として、「左上から順に走査して○も×も書かれていない場所に手を打つ」方法で実装しました。
実装した make_turn.sv
から該当部分を抜粋したのが次のコード片です.always
文の中で、1クロックに1マスずつ、 board_a_r[pt] == 0 && board_b_r[pt] == 0
でマスの状態を確認して、○も×もなければ、mask
との OR を取ることでビットを立て、手を打った状態に更新しています。
CHECK: begin
if(board_a_r[pt] == 0 && board_b_r[pt] == 0) begin
if(target_a_r == 1) begin
board_a_r <= board_a_r | mask;
end else begin
board_b_r <= board_b_r | mask;
end
state <= EMIT;
end else begin
if(pt == COLS*ROWS-1) begin
// 最後の候補を試したのになかった
error <= 1;
state <= EMIT;
end else begin
pt <= pt + 1; // 次の候補を試してみる
mask <= {mask[ROWS*COLS-1-1:0],1'b0}; // 次の手は左シフトで得る
end
end
end
手を決めるモジュールでは、○と×のどちらをアップデートするか入力時に決められるよう target_a
というフラグを用意しました。target_a
が 1
なら board_a
をアップデート、0
なら board_b
をアップデートします。また、打てるマスがない場合には error_flag
を立てるようにしましょう。
動作をシミュレーションした様子が次の通りです。
これは、3つのシナリオでのシミュレーションをした結果です。
1回目は、盤上に何も状態 board_a
も board_b
も 000000000
で、board_a
の手を決めようとしています(target_a
が 1
なので)。正しく board_a_out
に 000000001
を出力できています。
2回目は、盤面の状態が board_a
が 000010101
で board_b
が 000101010
のときに board_b
の手を決めようとしています(target_a
が 0
)。盤に何もない場所を探して着手しますので、board_b_out
の 7bit 目に 1
を立てて 001101010
を出力しています。
3回目は、盤面の状態が board_a
が 111010101
で board_b
が 000101010
のときの手を探そうとしています。空いているマスがないため error_flag
が 1
になっていることがわかります。
ゲームの判定
make_judge_i
は、与えられたゲーム状態から、
- ○と×のどちらかが勝利しているか
- どちらも勝利していない場合にゲームが継続できるかどうか
を判定するモジュールです。
動作をシミュレーションした様子が次の通りです。
4つのケースでシミュレーションしています。それぞれ
board_a
とboard_b
がともに000000000
の時 → 勝者はなくゲームの終了でもないboard_a
が 横1列をつくっている → 勝者を示すwin_a
が1
でゲームも終了(end_of_game
が1
)board_b
が 横1列をつくっている→ 勝者を示すwin_b
が1
でゲームも終了(end_of_game
が1
)- どちらもそろっていないがマスが全部埋まった → 勝者がなくゲームが終了(
end_of_game
が1
)
という動作が見て取れます。
勝者の判定方法を抜粋してみてみましょう。ゲームの状態として、○が置かれている場所と×が置かれてる場所を、それぞれのビット列で保持しています。そのため、それぞれのビット列に対して、縦・横・斜めに揃っているかを判定すれば、○あるいは×が勝利しているかどうかを決めることができます。Verilog の関数を使うと、
function [0:0] check_win(input [ROWS*COLS-1:0] board);
if({board[0], board[1], board[2]} == 3'b111 ||
{board[3], board[4], board[5]} == 3'b111 ||
{board[6], board[7], board[8]} == 3'b111 ||
{board[0], board[3], board[6]} == 3'b111 ||
{board[1], board[4], board[7]} == 3'b111 ||
{board[2], board[5], board[8]} == 3'b111 ||
{board[0], board[4], board[8]} == 3'b111 ||
{board[2], board[4], board[6]} == 3'b111) begin
check_win = 1'b1;
end else begin
check_win = 1'b0;
end
endfunction : check_win
のようにして、勝利しているかどうか判定することができます。
また、ゲームが継続できるかどうかの判定は、○と×のビット列の OR を取り、全ビット立っているかどうかで求めることができます。Verilog では、
(&(board_a | board_b) == 1)
と実装できます。前置演算子の &
は、リデュース積 (AND) という演算をする記号で、ビット列を構成する全ビットの積 (AND) を取って 1bit の 1
か 0
を得る回路を作ってくれます。
結果の表示
ゲームが終了したときに、勝者または引き分けの表示を出力するモジュールが print_result_i
です。結果を受け取って、結果に対応する文字列を出力します。
動作の様子をシミュレーションした様子が次の通りです。
このモジュールも3つのパタンでの動作をシミュレーションしています。順にプレーヤ2が勝った場合、プレーヤー1が勝った場合、引き分けだった場合のメッセージ出力結果が確認できています。
まとめ
今回は○×ゲームの実装の中身を、それぞれのシミュレーション結果も紹介しながら説明しました。実装したい対象(今回は○×ゲーム)を、必要な機能に分割して、それらを組み合わせることで実装する雰囲気を味わってもらえると幸いです。
FPGA 上にアプリケーションを載せる方法は VHDL や Verilog を使った RTL による実装方法だけではありません。FPGA 上でプロセッサを動かしその上でソフトウェアを走らせる、高位合成処理系を利用する、方法もあります。
○×ゲームで FPGA 開発をはじめてみようでは、どちらのアプローチの開発フローも紹介しています。いざ自分で FPGA 上にアプリケーションを実装しなければいけないという時に最適なアプローチが取れるよう、実装方法の選択肢を増やしておくとよいでしょう。是非○×ゲームの実装を題材にツールの使い方を練習してみてください。
わさらぼ・みよしたけふみ