Arduinoアプリケーション開発手順 パート4

前回のパート「Arduino Application Development Procedure Part-3」では、トラブルシューティングの問題を回避するために、入力と出力をテストすることについて簡単に説明しました。今回のステップでは、プレーヤー間で適切なシーケンスが確実に行われるようにするためのコードを提供し、問題点についてのコメントをします。コンセプトは次のようなものです。

  1. ステップ1:ライトは点灯していない。最初のプレーヤーが、LEDマトリクスの位置に対応するボタンを押すのを待ちます。
  2. ステップ2:プレーヤー1が着手し、1つのライト(赤)が点灯する。2番目のプレーヤーがLEDマトリクスの異なる位置に対応するボタンを押すのを待ちます。
  3. ステップ3:プレーヤー2が手を指し、1つのライト(青)が点灯する。一人が勝つか、両者が引き分けるまで、ステップ1、2を繰り返します。

この投稿では、私が抱えていた問題を議論し、少しでも読みやすくするために、コードのセクションを提供します。最後に、ご参考にコード全体をzipで圧縮したフォルダを提供します。

セクション1:セットアップとグローバル変数

/*
   This stage of the design I figure out the basic methodology to switch between players and change color per turn, set no winning actions, just a phase to
   figure out sequencing of player control. This stage took a lot of troubleshooting and method testing. In post I'll explain I had issues with certain areas, but wont
   include code because I forgot the old code that broke everything.
*/
//define red output pins and name them with short hand for Row number and Column number
#define RR3C3 53
#define RR3C2 52
#define RR3C1 51
#define RR2C3 50
#define RR2C2 49
#define RR2C1 48
#define RR1C3 47
#define RR1C2 46
#define RR1C1 45
//define blue output pins and name them with short hand for Row number and Column number
#define BR1C1 22
#define BR1C2 23
#define BR1C3 24
#define BR2C1 25
#define BR2C2 26
#define BR2C3 27
#define BR3C1 28
#define BR3C2 29
#define BR3C3 30
//define input pins and name them with shorthand for Row number and Column number
#define R3C1IN 34
#define R3C2IN 3
#define R3C3IN 4
#define R2C1IN 35
#define R2C2IN 6
#define R2C3IN 7
#define R1C1IN 8
#define R1C2IN 9
#define R1C3IN 10
//initialize two 3 by 3 matrices to keep track of the board, easy for logic; make the matrix empty to start with since nobody has made a move yet
const int rows = 3;
const int columns = 3;
boolean ttt1[ rows ][ columns ] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; //matrix for 3x3 tracking for player 1
boolean ttt2[ rows ][ columns ] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; //matrix for 3x3 tracking for player 2
boolean playerSw = 0; //variable for tracking which player is taking a turn
int lightCount = 0; //variable for counting how many lights are currently on
boolean flagTurn1P1 = 0; //variable for turn1 player1, will help with logic so I don't override past turns
boolean flagTurn2P2 = 0; //you can only ever have 9 turns max in a game of tic-tac-toe, need to keep track of who took a turn and where it took place; aslo specific turns exist for specific players in paper tic-tac-toe, so I named them appropriately
boolean flagTurn3P1 = 0;
boolean flagTurn4P2 = 0;
boolean flagTurn5P1 = 0;
boolean flagTurn6P2 = 0;
boolean flagTurn7P1 = 0;
boolean flagTurn8P2 = 0;
boolean flagTurn9P1 = 0;
void setup() {
  //set all the appropriate OUTPUT pins for the LED matrix, used R for Red and B for blue, then referred to short hand to indicate position in matrix for ease of use
  pinMode(RR1C1, OUTPUT);
  pinMode(RR1C2, OUTPUT);
  pinMode(RR1C3, OUTPUT);
  pinMode(RR2C1, OUTPUT);
  pinMode(RR2C2, OUTPUT);
  pinMode(RR2C3, OUTPUT);
  pinMode(RR3C1, OUTPUT);
  pinMode(RR3C2, OUTPUT);
  pinMode(RR3C3, OUTPUT);
  pinMode(BR1C1, OUTPUT);
  pinMode(BR1C2, OUTPUT);
  pinMode(BR1C3, OUTPUT);
  pinMode(BR2C1, OUTPUT);
  pinMode(BR2C2, OUTPUT);
  pinMode(BR2C3, OUTPUT);
  pinMode(BR3C1, OUTPUT);
  pinMode(BR3C2, OUTPUT);
  pinMode(BR3C3, OUTPUT);
  //set all the inputs for appropriate matrix positions, only need 9 of them because I don't need separate buttons for different players
  pinMode(R3C1IN, INPUT);
  pinMode(R3C2IN, INPUT);
  pinMode(R3C3IN, INPUT);
  pinMode(R2C1IN, INPUT);
  pinMode(R2C2IN, INPUT);
  pinMode(R2C3IN, INPUT);
  pinMode(R1C1IN, INPUT);
  pinMode(R1C2IN, INPUT);
  pinMode(R1C3IN, INPUT);
  //use serial for potential debugging issues
  Serial.begin(9600);
}

セットアップについていくつか言いたいことがあるのですが、どのコードも問題を起こさなかったのに、最終的なグローバル変数を何にするかを決めるのは一苦労でした。もともと、チックタックトーのボード全体を把握するためのマトリクス(2次元配列)は1つしか持っていなかったのですが、この方法は、特定のプレーヤーの差し手を追跡するのに適していないことがわかりました。プレーヤー2のために2つ目のユニークなマトリクスを用意することで、コーディングがずっと簡単になりました。もともと、何個のライトがすでに点灯しているかを追跡することは考えていませんでしたが(lightCountは当初の計画にはありませんでした)、何個のライトがすでに点灯しているかを知れば、シーケンスにかなり役立つことがわかりました。また、プレーヤーが適切なタイミングで勝ったかどうかをいつチェックするかをプログラムして、コードを減らすこともできるようになります。それぞれの手番のフラグも、結局は私のプログラミングプロセスの一部になっています。 プログラムが複雑で長くなったとき、冗長なシステムやコードがあれば、「バグ」が発生しません。グローバル変数を理解するためには、繰り返し実験する必要があります。

セクション2:メインループ

void loop() {
  //blank slate game state (beginning of game check); this checks if player 1's array is empty (other player's is empty anyway, don't need to check) and constantly reads each input
  //as soon as one of the buttons is pressed it will still read the pin, but now one of the values will be 1, therefor it will not allow the if statement to continue which is desired behavior
  if (ttt1[0][0] == 0 && ttt1[0][1] == 0 && ttt1[0][2] == 0 && ttt1[1][0] == 0 && ttt1[1][1] == 0 && ttt1[1][2] == 0 && ttt1[2][0] ==  0 && ttt1[2][1] == 0 && ttt1[2][2] == 0 && lightCount == 0 && playerSw == 0) {
    updateMatrix(); //update the appropriate 3x3 matrix by reading the input for buttons for all positions, see updateMatrix function below
  } else { //as soon as one of the values changes the game has begun
    if (lightCount == 0) { //run the update light FCN once because player one will have made a move, check if no lights are lit yet to run the first function, see updateFirstMove below
      updateFirstMove();
    } else if (lightCount == 1 && flagTurn2P2 != 1 && playerSw == 1) { //check if the light count is 1, make sure the flag for Player 2 for this turn is not set yet and make sure the right player is playing
      buttonPressAndMatrixUp(playerSw, lightCount); //check if buttons are pressed, update appropriate matrices, uses current player and lightCount as arguments, see function below
    } else if (lightCount == 2 && flagTurn3P1 != 1 && playerSw == 0) { //each turn has a pattern, check current light count, make sure appropriate turn flag is not set yet, and make sure right player is playing
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 3 && flagTurn4P2 != 1 && playerSw == 1) {
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 4 && flagTurn5P1 != 1 && playerSw == 0){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 5 && flagTurn6P2 != 1 && playerSw == 1){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 6 && flagTurn7P1 != 1 && playerSw == 0){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 7 && flagTurn8P2 != 1 && playerSw == 1){
      buttonPressAndMatrixUp(playerSw, lightCount);
    } else if (lightCount == 8 && flagTurn9P1 != 1 && playerSw == 0){
      buttonPressAndMatrixUp(playerSw, lightCount);
      Serial.println("TTT1:"); //print the matrix after 8 lights are lit, once the last button is pressed the output to the monitor will stop, the last TTT1: and TTT2: should reflect the same pattern seen on the lights with 1's and 0's
      printMat(ttt1);
      Serial.println("TTT2:");
      printMat(ttt2);
    }
  }
}

メインループが比較的シンプルなのは、使用する関数の開発に時間をかけたからです。メインループはできる限りシンプルにしたいのです。最初の手番ではライトが点灯していないのが特徴なので、最初の手番に特化した関数を2つ作りました。プレーヤー1のマトリクスが空(どの位置もまだゼロ)の場合、その位置に対応するデジタル入力を読むのと等しい位置にマトリクスの位置を設定します。R1C1INはttt1[0][0]に、R1C2INはttt1[0][1]に、R1C3INはttt1[0][2]に、R2C1INはttt1[1][0]、といったように対応します。コード内のコメントを読んでシーケンスを理解することをお勧めします。以下のセクションでは、私が作成したさまざまな関数について説明します。結局、私がメイン コードを開発する際には最も一般的な次の手順になります。メインループでコードの作成を開始しテストします(関数を最初から作成するのは難しい場合があるため、関数は使用しません)。次に出力結果のトラブルシューティングを通じてロジック内の誤りを見つけます。失敗したらいろいろ変更して必要なコードを何回か繰り返して試します。コードを書き直してもどうにも動作しない場合は、異なるロジックでコードを書き直し、最後に、同じ一般的なロジックを使用して、繰り返されたコードを関数に変換します。

セクション3:手番1の関数

//generic function to call for updating any lights after a move has been made, loops through the matrix and checks which player had played with playerSw so the appropriate color is showing, fine for turn 0 since no lights are on anyway
void updateFirstMove() {
  for (int i = 0; i < rows; i++) { //loop through rows
    for (int j = 0; j < columns; j++) { //loop through columns
      if (i == 0 && j == 0 && ttt1[i][j] == 1) { //check each position in the matrix individually by checking index values and if any of the matrix positions has been updated to 1
        digitalWrite(RR1C1, HIGH); //set appropriate light to be on (Red for player 1)
        flagTurn1P1 = 1; //update the flag for Turn1
      } else if (i == 0 && j == 1 && ttt1[i][j] == 1) { //same process happens for each index position
        digitalWrite(RR1C2, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 0 && j == 2 && ttt1[i][j] == 1) {
        digitalWrite(RR1C3, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 1 && j == 0 && ttt1[i][j] == 1) {
        digitalWrite(RR2C1, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 1 && j == 1 && ttt1[i][j] == 1) {
        digitalWrite(RR2C2, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 1 && j == 2 && ttt1[i][j] == 1) {
        digitalWrite(RR2C3, HIGH);
      } else if (i == 2 && j == 0 && ttt1[i][j] == 1) {
        digitalWrite(RR3C1, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 2 && j == 1 && ttt1[i][j] == 1) {
        digitalWrite(RR3C2, HIGH);
        flagTurn1P1 = 1;
      } else if (i == 2 && j == 2 && ttt1[i][j] == 1) {
        digitalWrite(RR3C3, HIGH);
        flagTurn1P1 = 1;
      }
    }
    if (lightCount == 0) { //check current lightCount, this will only allow lightCount to increment once (important for sequencing)
      lightCount++;
    }
    playerSw = 1; //switch player 
  }
}
//update the matrix with no conditions, good for turn 0, this just literally sets any player 1 matrix position to reading the appropriate input
void updateMatrix() {
  ttt1[0][0] = digitalRead(R1C1IN);
  ttt1[0][1] = digitalRead(R1C2IN);
  ttt1[0][2] = digitalRead(R1C3IN);
  ttt1[1][0] = digitalRead(R2C1IN);
  ttt1[1][1] = digitalRead(R2C2IN);
  ttt1[1][2] = digitalRead(R2C3IN);
  ttt1[2][0] = digitalRead(R3C1IN);
  ttt1[2][1] = digitalRead(R3C2IN);
  ttt1[2][2] = digitalRead(R3C3IN);
}

このセクションは作業しやすく、最初の手番のシーケンスをどのようにプログラムするか、そのイメージをつかむために最初に書いたものです。

セクション4:2番目から9番目までの手番シーケンス

//use following code to check if a button is pressed and update the appropriate matrix beyond the first turn, accepts the player and light count
void buttonPressAndMatrixUp(boolean player, int lc) {
  int reps = 0; //set a repetition variable for the function, important for sequencing
  for (int i2 = 0; i2 < rows; i2++) { //loop through rows
    for (int j2 = 0; j2 < columns; j2++) { //loop through columns
      if (player == 0) { //first check which player is making a move
        if (ttt1[i2][j2] == 1 || ttt2[i2][j2] == 1) { //check if any position in either matrix is already equal to 1, do nothing if this occurs (prevents cheating and good for sequencing)

        } else if (ttt1[i2][j2] == 0 && ttt2[i2][j2] == 0) { //check if both positions are 0 in both matrixes (just as a redundancy check)
          if (i2 == 0 && j2 == 0) { //again we need to check each index of the appropriate matrix (ttt1 belongs to player 1, ttt2 belongs to player 2
            reps = 0; //reset the reps to 0 at the beginning of this step, makes sure it can run again the next time the loop runs
            turnLightUp(i2, j2, R1C1IN, lc, RR1C1, player, reps); //update the appropriate light based on index values, correct input pin, correct light count, which light is being turned on, which player, and what the current repition value is; see fcn below
          } else if (i2 == 0 && j2 == 1) { //the same process is repeated for all the other index values for the matrix ttt1; appropriate inputs, light count, output lights, players, and reps are referenced
            reps = 0;
            turnLightUp(i2, j2, R1C2IN, lc, RR1C2, player, reps);
          } else if (i2 == 0 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R1C3IN, lc, RR1C3, player, reps);
          } else if (i2 == 1 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R2C1IN, lc, RR2C1, player, reps);
          } else if (i2 == 1 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R2C2IN, lc, RR2C2, player, reps);
          } else if (i2 == 1 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R2C3IN, lc, RR2C3, player, reps);
          } else if (i2 == 2 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R3C1IN, lc, RR3C1, player, reps);
          } else if (i2 == 2 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R3C2IN, lc, RR3C2, player, reps);
          } else if (i2 == 2 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R3C3IN, lc, RR3C3, player, reps);
          }
        }
      } else if (player == 1) { //do the same process for player 1, just make sure the right matrix is being updated and checked, light count being checked, correct color and R and column being referenced; altered same code as above
        if (ttt1[i2][j2] == 1 || ttt2[i2][j2] == 1) {

        } else if (ttt2[i2][j2] == 0 && ttt1[i2][j2] == 0) {
          if (i2 == 0 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R1C1IN, lc, BR1C1, player, reps);
          } else if (i2 == 0 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R1C2IN, lc, BR1C2, player, reps);
          } else if (i2 == 0 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R1C3IN, lc, BR1C3, player, reps);
          } else if (i2 == 1 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R2C1IN, lc, BR2C1, player, reps);
          } else if (i2 == 1 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R2C2IN, lc, BR2C2, player, reps);
          } else if (i2 == 1 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R2C3IN, lc, BR2C3, player, reps);
          } else if (i2 == 2 && j2 == 0) {
            reps = 0;
            turnLightUp(i2, j2, R3C1IN, lc, BR3C1, player, reps);
          } else if (i2 == 2 && j2 == 1) {
            reps = 0;
            turnLightUp(i2, j2, R3C2IN, lc, BR3C2, player, reps);
          } else if (i2 == 2 && j2 == 2) {
            reps = 0;
            turnLightUp(i2, j2, R3C3IN, lc, BR3C3, player, reps);
          }
        }
      }
    }
  }
}
//following function accepts the row and column being checked, which input is being read, what the light count is, output pin being modified, player, and repetition amount to update the appropriate matrix, turn the right light on, and set flags for next player move
void turnLightUp(int row, int col, int inPin, int lc, int outPin, int player, int reps) {
  while (reps < 1) { //set a repetition loop so all the code below is repeated once each time (more than once causes errors sometimes where a light is lit both blue and red)
    if (player == 0) { //check player again for redundancy
      ttt1[row][col] = digitalRead(inPin); //update the ttt1 matrix with digitalread of the appropriate pin (remember row, col, and inPin are local to this function, so whatever value is passed in will be referenced)
      if (ttt1[row][col] == 1 && lc == 2) { //if the value is updated to one after previous step and the appropriate light count is equal to specific value based on turn, do the following 
        digitalWrite(outPin, HIGH); //update the referenced outPin to HIGH
        if (lightCount == 2) { //update the light count once, important for sequencing
          lightCount++;
        }
        flagTurn3P1 = 1; //set the appropriate flag to 1
        playerSw = 1; //switch player
      } else if (ttt1[row][col] == 1 && lc == 4) { //we always check if the value updated is 1, but the light count changes per turn and certain light counts only happen with certain players, this repeated action is done for unique turns with the same code below
        digitalWrite(outPin, HIGH);
        if (lightCount == 4) {
          lightCount++;
        }
        flagTurn5P1 = 1;
        playerSw = 1;
      } else if (ttt1[row][col] == 1 && lc == 6) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 6) {
          lightCount++;
        }
        flagTurn7P1 = 1;
        playerSw = 1;
      } else if (ttt1[row][col] == 1 && lc == 8) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 8) {
          lightCount++;
        }
        flagTurn9P1 = 1;
        playerSw = 1;
      }
    } else if (player == 1) { //have to do the same process for the other player of course, just refer to different light counts
      ttt2[row][col] = digitalRead(inPin);
      if (ttt2[row][col] == 1 && lc == 1) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 1) {
          lightCount++;
        }
        flagTurn2P2 = 1;
        playerSw = 0;
      } else if (ttt2[row][col] == 1 && lc == 3) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 3) {
          lightCount++;
        }
        flagTurn4P2 = 1;
        playerSw = 0;
      } else if (ttt2[row][col] == 1 && lc == 5) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 5) {
          lightCount++;
        }
        flagTurn6P2 = 1;
        playerSw = 0;
      } else if (ttt2[row][col] == 1 && lc == 7) {
        digitalWrite(outPin, HIGH);
        if (lightCount == 7) {
          lightCount++;
        }
        flagTurn8P2 = 1;
        playerSw = 0;
      }
    } //finally after the while loop finishes update reps by incrementing by one, this will allow it to reach the next while loop in the next call of the function above, it's also important that incrementing does not matter which player is playing, so it is outside of everything above
    reps++;
  }
}
//print the matrix input into the function, I just used this to make sure the values match what the lights show for all 9 lights being on, won't be in final product
void printMat(boolean a[rows][columns]){
  for (int i = 0; i < rows; i++){
    for (int j = 0; j < columns; j++){
      Serial.print(a[i][j]);
      Serial.print("\n");
    }
  }
}

最初の手番以降のシーケンスの書き方を考えるのが非常に難しく、デバッグやトラブルシューティングの回数も一番多くなりました。「buttonPressAndMatrixUp」関数を書いているときに、どちらかのプレーヤーのマトリクスを検索するためのfor()ループが原因不明で壊れてしまうことがありました。ある時点でi1かj1のどちらかに1を追加し続けることはできませんでした(何が起こっているのか突き止められませんでした)。考えているようにいかず、何が原因かわからないほど壊れるコードがある場合、それが解決するまで小さな編集も加えることはお勧めしません。これを試しましたが、ほとんどあきらめていました。別のアプローチでロジックを試したところ、うまくいきました。buttonPressAndMatrixUpの最終版をテストした後、turnLightUp()という2番目の関数を書いて、各要素が適切なマトリクスでチェックされたときに使用することで、総行数を減らすようにしました。

この時点では、コード全体に「勝ち」の条件はありません。まず、最初に交代するときの動作をどうコーディングするか考える必要がありました。プロセス全体を一度に理解しようとするよりもむしろ、長いコードを小さな部分に分けて取り組む方が現実的です。次回は、勝利と引き分けの条件や、勝利時のライトの演出など、欲しい機能を盛り込んだ最終バージョンをお届けする予定です。

コード全体のzipファイルです。Arduinoのコードをリネームする場合は、プログラムと同じ名前のフォルダにすることを忘れないでください(すでにzipに含まれています)。testTTTBasic_Up.zip(4.1 KB)




オリジナル・ソース(English)