Arduinoでマルバツを作る

Arduino

GUIミニゲームを作ってみよう

TFTタッチスクリーンの簡単な応用例として、GUIゲームを作成してみます。

Arduinoで○×

まずは思考アルゴリズムの簡単な『○×(マルバツ、三目並べ)』です。

できるだけプログラムを簡単にするため、プレイヤーが『○』で先行なのは固定です。
操作も簡単に、プレイヤーが画面上の盤面の○を描きたい場所をペンでタップするだけとします。
どちらかが3並びを完成するか、盤面がすべて埋まるとゲーム終了で、勝敗が表示されます。
画面下の『TAP HERE』という箇所をタップするとまた最初からゲームが開始されます。

プログラム

思いつきで適当に作ったのでかなりだらだらとしたプログラムですが…。

C++
#include "Elegoo_GFX.h"
#include "Elegoo_TFTLCD.h"
#include "TouchScreen.h"

// ピンの定義
// 表示機能用
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RST A4

// タッチ機能用
#define YP A3
#define XM A2
#define YM 9
#define XP 8

// タッチ検出および座標変換用
#define MINPRESSURE 10
#define MAXPRESSURE 1000
#define TS_MINX 120
#define TS_MAXX 900
#define TS_MINY 70
#define TS_MAXY 920

// 色の定義
#define	BLACK   0x0000
#define	BLUE    0x001F
#define	RED     0xF800
#define	GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF

// インスタンスの生成
Elegoo_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RST);
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

// ゲーム用定義
#define FREE 0
#define PLAYER 1
#define COMP -1
#define MAX_DEPTH 2
#define MYSELF_WIN 32767
#define ENEMY_WIN -32767
#define PLAYER_WIN 1
#define COMP_WIN -1
#define DRAW 255
#define CONT 0

// ゲーム用変数
int board[9];
int line[][3]={{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};

// 現在の盤面の状態がside側にとってどれくらい有利かを評価する。
int eval_board(int side){
  int m[]={0,0,0,0},e[]={0,0,0,0},eval;
  int _m,_e;
  for(int i=0; i<8; i++){
    _m=_e=0;
    for(int j=0; j<3; j++){
      if(board[line[i][j]] == side){
        _m++;
      }else if(board[line[i][j]] != FREE){
        _e++;
      }
    }
    if(_e==0){
      m[_m]++;
    }else if(_m==0){
      e[_e]++;
    }
  }
  if(m[3]>0){
    eval = MYSELF_WIN;
  }else if(e[3]>0){
    eval = ENEMY_WIN;
  }else{
    eval = m[2]*15-e[2]*7+m[1]*3-e[1];
  }
  return eval;
}

// 終了チェック
int end_check(){
  int eval = eval_board(PLAYER);
  if(eval == MYSELF_WIN){ return PLAYER_WIN; }
  if(eval == ENEMY_WIN){ return COMP_WIN; }
  for(int i=0; i<9; i++){
    if(board[i] == FREE){ return CONT; }
  }
  return DRAW;
}

// COMP側の思考
int think_comp(int side, int depth){
  int maxscore = ENEMY_WIN;
  int hand = -1;
  int score;
  if(depth == 0){
    return eval_board(side);
  }
  for(int i=0; i<9; i++){
    if(board[i] == FREE){
      board[i] = side;
      score = -think_comp(-side, depth-1);
      if(score > maxscore){
        maxscore = score;
        hand = i;
      }
      board[i]=FREE;
    }
  }
  if(hand != -1 && depth == MAX_DEPTH){
    board[hand] = side;
    return 0;
  }else if(hand == -1){
    return eval_board(side);
  }
  return maxscore;
}

// 盤面の表示
void disp(){
  tft.fillScreen(WHITE);
  tft.drawFastHLine(0,80,240,BLACK);
  tft.drawFastHLine(0,160,240,BLACK);
  tft.drawFastVLine(80,0,240,BLACK);
  tft.drawFastVLine(160,0,240,BLACK);

  for(int i=0; i<9; i++){
    int x=(i%3)*80+40;
    int y=(i/3)*80+40;
    int ch=board[i];
    switch(ch){
      case -1:
        tft.drawLine(x-30,y-30,x+30,y+30,RED);
        tft.drawLine(x+30,y-30,x-30,y+30,RED);
        break;
      case 1:
        tft.drawCircle(x,y,30,BLUE);
        break;
    }
  }
}

// タップ位置の取得(タップされるまで待つ)
void getTapPoint(int *x, int *y){
  while(true){
    digitalWrite(13, HIGH);
    TSPoint p = ts.getPoint();
    digitalWrite(13, LOW);
    pinMode(XM, OUTPUT);
    pinMode(YP, OUTPUT);

    if(p.z>MINPRESSURE && p.z<MAXPRESSURE){
      *x = map(p.x, TS_MINY, TS_MAXY, 0, tft.width()-1);
      *y = tft.height()-map(p.y, TS_MINX, TS_MAXX, 0, tft.height()-1);
      return;
    }
  }
}

// ゲーム用変数の初期化
void game_init(){
  for(int i=0; i<9; i++){
    board[i] = FREE;
  }
  disp();
}

// 文字列表示
void drawText(int x, int y, int size, int color, char* text){
  tft.setTextColor(color);
  tft.setTextSize(size);
  tft.setCursor(x, y);
  tft.println(text);
}

// ゲーム終了後のタップ待ち
void waitTap(){
  tft.drawRect(72, 300, 96, 16, RED);
  drawText(72, 301, 2, RED, "TAP HERE");
  while(true){
    int x, y;
    getTapPoint(&x, &y);
    if(x>72 && x<168 && y>300){
      break;
    }
  }
  game_init();
}

// 初期設定
void setup() {
  // TFTの初期化
  tft.reset();
  uint16_t identifier = tft.readID();
  if(identifier==0x0101){     
    identifier=0x9341;
  }else if(identifier==0x1111){     
    identifier=0x9328;
  }
  tft.begin(identifier);
  tft.setRotation(0);

  game_init();
}

// メインループ
void loop() {
  int hand;

  while(true){
    int x,y;
    getTapPoint(&x, &y);
    hand = (y/80)*3+(x/80);
    if(0 <= hand && hand < 9 && board[hand] == FREE)break;
  }
  board[hand] = PLAYER;
  disp();

  int score = end_check();
  if(score == CONT){
    think_comp(COMP, MAX_DEPTH);
    disp();
    score = end_check();
  }
  if(score == PLAYER_WIN){
    drawText(0, 260, 2, BLUE, "PLAYER WIN!");
    waitTap();
  }else if(score == COMP_WIN){
    drawText(0, 260, 2, RED, "Arduino WIN!");
    waitTap();
  }else if(score == DRAW){
    drawText(0,260,2,GREEN,"DRAW!");
    waitTap();
  }
}

プログラムの解説

ライブラリのインクルード、定数の設定など
C++
#include "Elegoo_GFX.h"
#include "Elegoo_TFTLCD.h"
#include "TouchScreen.h"

// ピンの定義
// 表示機能用
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RST A4

// タッチ機能用
#define YP A3
#define XM A2
#define YM 9
#define XP 8

// タッチ検出および座標変換用
#define MINPRESSURE 10
#define MAXPRESSURE 1000
#define TS_MINX 120
#define TS_MAXX 900
#define TS_MINY 70
#define TS_MAXY 920

// 色の定義
#define	BLACK   0x0000
#define	BLUE    0x001F
#define	RED     0xF800
#define	GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF

// インスタンスの生成
Elegoo_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RST);
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

ここまではタッチスクリーンの解説回と同じです。

ゲーム用の定数・グローバル変数
C++
// ゲーム用定義
#define FREE 0
#define PLAYER 1
#define COMP -1
#define MAX_DEPTH 2
#define MYSELF_WIN 32767
#define ENEMY_WIN -32767
#define PLAYER_WIN 1
#define COMP_WIN -1
#define DRAW 255
#define CONT 0

// ゲーム用変数
int board[9];
int line[][3]={{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};

ゲーム用の定数を設定しています。FREEPLAYERCOMP は盤面の状態を表し、
FREE は空白
PLAYER は『○』が描かれている
COMP は『×』が描かれている
という意味です。

MAX_DEPTH は先読みの手数です。

MYSELF_WIN、ENEMY_WIN は盤面の状態の判定に使用しています。

PLAYER_WIN、COMP_WIN、DRAW、CONT は現在のゲームの状態(どちらかが勝ち、引き分け、ゲーム続行中)を表しています。

配列変数 board[] は一次元配列ですが、

のように3×3の盤面の情報を格納しています。

変数line は、3並びができる盤面の番号を表しています。たとえば {0,1,2} は、0番・1番・2番に同じマークが描かれていたら3並び完成、ということを表しています。

盤面の評価
C++
// 現在の盤面の状態がside側にとってどれくらい有利かを評価する。
int eval_board(int side){
  int m[]={0,0,0,0},e[]={0,0,0,0},eval;
  int _m,_e;
  for(int i=0; i<8; i++){
    _m=_e=0;
    for(int j=0; j<3; j++){
      if(board[line[i][j]] == side){
        _m++;
      }else if(board[line[i][j]] != FREE){
        _e++;
      }
    }
    if(_e==0){
      m[_m]++;
    }else if(_m==0){
      e[_e]++;
    }
  }
  if(m[3]>0){
    eval = MYSELF_WIN;
  }else if(e[3]>0){
    eval = ENEMY_WIN;
  }else{
    eval = m[2]*15-e[2]*7+m[1]*3-e[1];
  }
  return eval;
}

eval_board(side,depth)関数はsideにとっての現在の盤面の有利度を計算します。3目並べなら最後まで読み切ることも可能なのですが、別のゲームへの応用も考えて途中の状態の盤面のスコアを計算するようにしました。実は計算式はテキトーなのですが、こんなものでも充分動きます。

終了チェック
C++
// 終了チェック
int end_check(){
  int eval = eval_board(PLAYER);
  if(eval == MYSELF_WIN){ return PLAYER_WIN; }
  if(eval == ENEMY_WIN){ return COMP_WIN; }
  for(int i=0; i<9; i++){
    if(board[i] == FREE){ return CONT; }
  }
  return DRAW;
}
手読み関数
C++
// COMP側の思考
int think_comp(int side, int depth){
  int maxscore = ENEMY_WIN;
  int hand = -1;
  int score;
  if(depth == 0){
    return eval_board(side);
  }
  for(int i=0; i<9; i++){
    if(board[i] == FREE){
      board[i] = side;
      score = -think_comp(-side, depth-1);
      if(score > maxscore){
        maxscore = score;
        hand = i;
      }
      board[i]=FREE;
    }
  }
  if(hand != -1 && depth == MAX_DEPTH){
    board[hand] = side;
    return 0;
  }else if(hand == -1){
    return eval_board(side);
  }
  return maxscore;
}
盤面の表示
C++
// 盤面の表示
void disp(){
  tft.fillScreen(WHITE);
  tft.drawFastHLine(0,80,240,BLACK);
  tft.drawFastHLine(0,160,240,BLACK);
  tft.drawFastVLine(80,0,240,BLACK);
  tft.drawFastVLine(160,0,240,BLACK);

  for(int i=0; i<9; i++){
    int x=(i%3)*80+40;
    int y=(i/3)*80+40;
    int ch=board[i];
    switch(ch){
      case -1:
        tft.drawLine(x-30,y-30,x+30,y+30,RED);
        tft.drawLine(x+30,y-30,x-30,y+30,RED);
        break;
      case 1:
        tft.drawCircle(x,y,30,BLUE);
        break;
    }
  }
}

board[]の内容にしたがって盤面を描画します。

タップ位置の取得
C++
// タップ位置の取得(タップされるまで待つ)
void getTapPoint(int *x, int *y){
  while(true){
    digitalWrite(13, HIGH);
    TSPoint p = ts.getPoint();
    digitalWrite(13, LOW);
    pinMode(XM, OUTPUT);
    pinMode(YP, OUTPUT);

    if(p.z>MINPRESSURE && p.z<MAXPRESSURE){
      *x = map(p.x, TS_MINY, TS_MAXY, 0, tft.width()-1);
      *y = tft.height()-map(p.y, TS_MINX, TS_MAXX, 0, tft.height()-1);
      return;
    }
  }
}

タップ位置座標の取得を関数化しました。処理自体は以前のものと同じですが、座標を2つの変数に返すためアドレス渡し引数を使用しています。。

ゲーム用の初期化
C++
// ゲーム用変数の初期化
void game_init(){
  for(int i=0; i<9; i++){
    board[i] = FREE;
  }
  disp();
}

ゲームを初期化します。単にboard[]を0で埋めるだけです。画面の再描画もしています。

文字列表示
C++
// 文字列表示
void drawText(int x, int y, int size, int color, char* text){
  tft.setTextColor(color);
  tft.setTextSize(size);
  tft.setCursor(x, y);
  tft.println(text);
}

色・場所・文字サイズを指定して文字列表示をする処理が多いので関数化しました。

ゲーム終了時のタップ待ち
C++
// ゲーム終了後のタップ待ち
void waitTap(){
  tft.drawRect(72, 300, 96, 16, RED);
  drawText(72, 301, 2, RED, "TAP HERE");
  while(true){
    int x, y;
    getTapPoint(&x, &y);
    if(x>72 && x<168 && y>300){
      break;
    }
  }
  game_init();
}

ゲーム終了後、画面最下部に『TAP HERE』と表示してからその部分がタップされるまで無限ループします。タップされたらゲームを初期化します。

setup()関数
C++
// 初期設定
void setup() {
  // TFTの初期化
  tft.reset();
  uint16_t identifier = tft.readID();
  if(identifier==0x0101){     
    identifier=0x9341;
  }else if(identifier==0x1111){     
    identifier=0x9328;
  }
  tft.begin(identifier);
  tft.setRotation(0);

  game_init();
}

setup()関数内ではtftそのものの初期化をしています。以前のプログラムと同じです。

メインループ(loop()関数)
C++
// メインループ
void loop() {
  int hand;

  while(true){
    int x,y;
    getTapPoint(&x, &y);
    hand = (y/80)*3+(x/80);
    if(0 <= hand && hand < 9 && board[hand] == FREE)break;
  }
  board[hand] = PLAYER;
  disp();

  int score = end_check();
  if(score == CONT){
    think_comp(COMP, MAX_DEPTH);
    disp();
    score = end_check();
  }
  if(score == PLAYER_WIN){
    drawText(0, 260, 2, BLUE, "PLAYER WIN!");
    waitTap();
  }else if(score == COMP_WIN){
    drawText(0, 260, 2, RED, "Arduino WIN!");
    waitTap();
  }else if(score == DRAW){
    drawText(0,260,2,GREEN,"DRAW!");
    waitTap();
  }
}

メインループの中では、

プレイヤーの手を入力
ゲームが終了しているか(どちらかの勝ちまたは引き分け)を判定
コンピュータの手を判断
ゲームが終了しているか(どちらかの勝ちまたは引き分け)を判定

を繰り返しています。ゲームが終了していたらメッセージを表示してタップされるまで待ちます。

実行結果

まぁわざとプレイヤーが負けない限り引き分けるのが○×というゲームなんですが(苦笑)
とりあえずArduinoでもそれっぽいGUI思考ゲームができるということは判ると思います。

コメント

タイトルとURLをコピーしました