/* LM February 2022 * License: Creative Commons: https://creativecommons.org/licenses/by/3.0/us/ */ // Platform: Teensy 3.5 // Fixed dimensions 6 rows, 7 columns #include #include #include // TFT #define TFT_DC 9 // N/C #define TFT_CS 10 // Display CS #define TFT_RST 255 // 255 = unused, connect to 3.3V #define TFT_MOSI 11 #define TFT_SCLK 13 #define TFT_MISO 12 // Not used (not connected) // LED pin between SCK and MISO on TFT is not connected //Touch #define T_CS 38 // Touch CS #define T_DO 29 // Touch data out (UTouch) #define T_DIN 30 // Touch data in (UTouch) #define T_CLK 36 // Touch clock (UTouch) #define T_IRQ 22 // Touch IRQ // Calibration (Copied from MOAK 3.0 sketch - Same TFT) #define X_PIXELS 320 #define Y_PIXELS 240 #define X_MIN 0 // Empirically determined minimum X returned by getX() for display part of screen #define Y_MIN 26 // Same for Y #define Y_MIN 20 // Same for Y #define X_MAX 230 // Etc. #define Y_MAX 390 #define Y_MAX 380 #define X_RANGE (X_MAX - X_MIN) #define Y_RANGE (Y_MAX - Y_MIN) // Instantiate ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK, TFT_MISO); UTouch ts(T_CLK, T_CS, T_DIN, T_DO, T_IRQ); // Standard colors #define BLACK 0x0000 #define BLUE 0x001F #define BLUE_GREY 0xc6d9 #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define WHITE 0xFFFF #define TAN 0x00FFF0 // Color aliases #define TFT_BG YELLOW #define TFT_TXT BLUE // Text color 1 #define TFT_ALT RED // Text color 2 // (TFT) GUI uint16_t lastX = -1; uint16_t lastY = -1; // Time constants const long ONESEC = 1000; const long TWOSEC = 2000; const long READ_PAUSE = 50; const long LED_PAUSE = 200; const long MIN_TIME_BETWEEN_TOUCHES = ONESEC; // Debug const boolean DEBUG_MM = false; const boolean DEBUG_MAX = false; const boolean DEBUG_TEST = true; const int LED = 35; int testColumn = 0; // Game parameters const int R = 6; // Rows and columns const int C = 7; const int INF = INT_MAX-1; // Close enough! // Game display #define GAME_ARRAY_LINE_COLOR BLACK #define GAME_ARRAY_BACKGROUND BLUE_GREY const uint16_t pieceColor[2] = {0x07E0, 0xF800}; const char FILE_LETTER[8] = {' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g'}; const int TOP_EDGE = 50; const int LEFT_EDGE = 90; const int COL_WIDTH = 25; const int CELL_HEIGHT = COL_WIDTH; const int COL_HEIGHT = R*CELL_HEIGHT; // Touch corrections const int LEFT_EDGE_TOUCH = 86; const int TOP_EDGE_TOUCH = 45; // Header const int HEADER_X = 105; const int HEADER_Y = 15; const int HEADER_TEXT_SIZE = 2; const int HEADER_TEXT_COLOR = BLUE; const char HEADER_TEXT[] = "Connect Four"; // Pieces const int PIECES_RADIUS = 12; const int PIECES_SHRINK_FACTOR = 1; const int PIECES_HOME_X = 50; const int PIECE1_HOME_Y = 185; const int PIECE2_HOME_Y = 155; // Touch corrections const int PIECES_HOME_TOUCH_X = 36; // Experimentally determined const int PIECE1_HOME_TOUCH_Y = 188; const int PIECE2_HOME_TOUCH_Y = 158; // All buttons const int BUTTON_RADIUS = 5; // Undo button const int UNDO_X = 20; const int UNDO_Y = 50; const int UNDO_W = 60; const int UNDO_H = 20; const int UNDO_TEXT_SIZE = 2; const uint16_t UNDO_TEXT_COLOR = BLACK; const uint16_t UNDO_BACKGROUND_COLOR = TAN; const char UNDO_TEXT[] = "Undo"; const int UNDO_TOUCH_X = 10; // Range 10 - 10 + UNDO_W const int UNDO_TOUCH_Y = 55; // Range 215 - 215 + UNDO_H // Redo button const int REDO_X = 20; const int REDO_Y = 80; const int REDO_W = 60; const int REDO_H = 20; const int REDO_TEXT_SIZE = 2; const uint16_t REDO_TEXT_COLOR = BLACK; const uint16_t REDO_BACKGROUND_COLOR = TAN; const char REDO_TEXT[] = "Redo"; const int REDO_TOUCH_X = 10; // Range 10 - 10 + REDO_W const int REDO_TOUCH_Y = 85; // Range 215 - 215 + REDO_H // Auto button const int AUTO_X = 20; const int AUTO_Y = 110; const int AUTO_W = 60; const int AUTO_H = 20; const int AUTO_TEXT_SIZE = 2; const uint16_t AUTO_TEXT_COLOR = BLACK; const uint16_t AUTO_BACKGROUND_COLOR = 0x00FFF0; // Tan String autoText = "Auto"; // Accommodate toggle switch text const int AUTO_TOUCH_X = 10; // Range 10 - 10 + AUTO_W const int AUTO_TOUCH_Y = 115; // Range 115 - 115 + AUTO_H // Test button const int TEST_X = 20; const int TEST_Y = 210; const int TEST_W = 60; const int TEST_H = 20; const int TEST_TEXT_SIZE = 2; const uint16_t TEST_TEXT_COLOR = BLACK; const uint16_t TEST_BACKGROUND_COLOR = 0x00FFF0; // Tan const char TEST_TEXT[] = "Test"; const int TEST_TOUCH_X = 10; // Range 10 - 10 + TEST_W const int TEST_TOUCH_Y = 215; // Range 215 - 215 + TEST_H // Clear button const int CLEAR_X = 270; const int CLEAR_Y = 180; const int CLEAR_W = 45; const int CLEAR_H = 20; const int CLEAR_TEXT_SIZE = 2; const uint16_t CLEAR_TEXT_COLOR = BLACK; const uint16_t CLEAR_BACKGROUND_COLOR = 0x00FFF0; // Tan const char CLEAR_TEXT[] = "CLR"; const int CLEAR_TOUCH_X = 280; // Range 280 - 280 + CLEAR_W const int CLEAR_TOUCH_Y = 185; // Range 185 - 185 + CLEAR_H // Status box coordinates and dimensions #define STATUS_TEXT_COLOR BLUE #define STATUS_TEXT_SIZE 2 const int STATUS_X = 120; const int STATUS_Y = 220; const int STATUS_W = 200; const int STATUS_H = 20; int game[R][C]; // Interactive game array int gameRecord[R*C]; // Linear list of column numbers int expandedGameRecord[R*C][2]; // Linear list of column, row numbers int mmGame[R][C]; // Working copy of game array for minimax int filledCellCount = 0; // Subscript for gameRecord int firstSelectedPiece = 0; // 1 = green piece, 2 = red piece int lastSelectedPiece = 0; // Last piece shown on board // Computer can play first or second (or both), therefore - int maxPlayerPiece = 0; // Assign piece# before calling minimax int minPlayerPiece = 0; // Ditto - the other piece# int lookaheadDepth = 0; // Modify before calling minimax int mmSelectedColumn = 0; // Assigned in minimax at topmost level int colScore[C]; // Score for each column at topmost level int winDepth = 0; // Search depth at which certain win/loss detected long lastTouch = 0; // Anti-noise boolean autoplayInProgress = false; // Toggle int autoplayOpponentPiece = 0; // Auto-select player's piece in autoplay mode const int ENDGAME = 26; // Game stage when filledCellCount >= this value const int MIN_DEPTH = 6; // Search depth at start of game const int OPENING = 6; // Number of plys (filledCellCount) before deepening search const float RECIPROCAL_SLOPE = 2.6; // Slope of increasing depth up to filledCellCount = ENDGAME // Physical game (Robot arm) interface const int NUM_OF_XDIN = 8; // [2] - [8] are column microswitches 1 - 7 const int XDIN[NUM_OF_XDIN] = {28, 2, 3, 4, 5, 6, 7, 8}; const int NUM_OF_XOUT = 4; // [24] - [26] are bits 0 - 2 and [27] is data ready const int XOUT[NUM_OF_XOUT] = {24, 25, 26, 27}; void setup() { Serial.begin(9600); tft.begin(); tft.setRotation (1); splash(); delay(TWOSEC); ts.InitTouch(PORTRAIT); ts.setPrecision(PREC_MEDIUM); initGame(); randomSeed(analogRead(A9)); for (int i=0; i 0) return; // Column is full int r; // Compute lowest clear row for (r=0; r M = 3 int v[4]; // Vector of four connected cells // Score middle column for (int i=0; i for piece# p=1, p=2 // Horizontal for (int i=0; i 0) { // If win for either player if (DEBUG_MM) Serial.println("Win for #" + String(w)); if (w == maxPlayerPiece) return +INF; else return -INF; } else if (isArrayFull(g)) { // Game is a tie return 0; } else if (d == 0) { // max ply lookahead reached (terminal node) if (DEBUG_MM) { Serial.println("Evaluate for piece#" + String(maxPlayerPiece)); printGameArray(g); } int s = score(g, maxPlayerPiece); if (DEBUG_MM) { Serial.print("Returning score = " + String(s)); Serial.println(); Serial.println(); } return s; } // Fall through here if not a terminal node if (m) { // Maximizing player if (DEBUG_MM) Serial.println("Maximizing player.."); int v = -INF; // Initialize value of play at current ply for (int j=0; j v) { if (DEBUG_MM) Serial.println("d=" + String(d) + ", j=" + String(j) + ", mm=" + String(mm)); v = mm; if (d == lookaheadDepth) mmSelectedColumn = j+1; // Column number 1 to C (not subscript) } if (alpha < v) alpha = v; if (alpha >= beta) break; } return v; // Best at current ply } else { // Minimizing player if (DEBUG_MM) Serial.println("Minimizing player.."); int v = +INF; // Initialize value of play at current ply for (int j=0; j 0) // Immediate win or forced block return col; lookaheadDepth = 6; // Modify to test at different depths maxPlayerPiece = pieceToPlay(); minPlayerPiece = otherPiece(maxPlayerPiece); copyGameArray(mmGame, game); // As argued to minimax lookaheadDepth = dynamicLookaheadDepth(); if (lookaheadDepth > MIN_DEPTH && filledCellCount < ENDGAME) displayStatus("mm: thinking"); // To do: Precheck mmGame for terminal node int mmScore = minimax(mmGame, lookaheadDepth, -INF, INF, true); col = mmSelectedColumn; if (mmScore == INF) { while (mmScore == INF && (lookaheadDepth -= 2) > 0) { // Find the shortest win col = mmSelectedColumn; // Before recomputing! mmScore = minimax(mmGame, lookaheadDepth, -INF, INF, true); } } else if (mmScore == -INF) { // Force choose play when all plays lead to loss displayStatus("mm: Not good"); while(mmScore == -INF && (lookaheadDepth -= 2) > 0) mmScore = minimax(mmGame, lookaheadDepth, -INF, INF, true); } String s; // Nicety if (mmScore == INF) s = "+INF"; else if (mmScore == -INF) s = "-INF"; else s = String(mmScore); // To do: Generalize to terminal node win/loss/tie, else play if (mmScore == -INF) displayStatus("mm: You win!"); else displayStatus("mm: " + String(col) + ", " + s); return col; } int winOrForcedBlock() { // Pre-check and bypass think if win for either player is imminent. // If computer can win on the move, or must block opponent's immediate win.. int row, myPiece = pieceToPlay(); int oPiece = otherPiece(myPiece); copyGameArray(mmGame, game); for (int j=0; j lastTouch) { lastTouch = millis(); processTouch(x, y); } } delay(20); } void processTouch(int x, int y) { // Assume x and y are non-negative int p = getPiece(x, y); if (p > 0) { displayStatus("Piece " + String(p) + " selected", GAME_ARRAY_BACKGROUND); lastSelectedPiece = p; } else { int c = getColumn(x, y); if (c > 0 && c < 8) { displayStatus("Col. " + String(c) + " selected", GAME_ARRAY_BACKGROUND); testColumn = c; // For testing robot arm interface if (lastSelectedPiece > 0) { dropPiece(game, lastSelectedPiece, c, true); lastSelectedPiece = 0; } else if (autoplayInProgress) { dropPiece(game, autoplayOpponentPiece, c, true); lastSelectedPiece = 0; } } else if (processUndo(x,y)) ; else if (processRedo(x,y)) ; else if (processAuto(x,y)) ; else if (processTest(x, y)) ; else if (processClear(x, y)) ; else displayStatus("X= " + String(x) + " Y= " + String(y), GAME_ARRAY_BACKGROUND); } } int getPiece(int x, int y) { // If x, y are within a piece's home location boundaries return that piece ID if ((PIECES_HOME_TOUCH_X - PIECES_RADIUS <= x) && (PIECES_HOME_TOUCH_X + PIECES_RADIUS >= x)) { if ((PIECE1_HOME_Y - PIECES_RADIUS <= y) && (PIECE1_HOME_Y + PIECES_RADIUS >= y)) return 1; if ((PIECE2_HOME_Y - PIECES_RADIUS <= y) && (PIECE2_HOME_Y + PIECES_RADIUS >= y)) return 2; } return 0; // No piece selected } int getColumn(int x, int y) { // If x, y are within a game array column boundary return that column ID if ((LEFT_EDGE_TOUCH <= x) && (x <= (LEFT_EDGE_TOUCH + (COL_WIDTH * 7)))) if ((TOP_EDGE_TOUCH <= y) && (y <= (TOP_EDGE_TOUCH + COL_HEIGHT))) return (x - LEFT_EDGE_TOUCH) / COL_WIDTH + 1; return 0; } boolean processUndo(int x, int y) { if ( y < UNDO_TOUCH_Y) return false; if ( y > (UNDO_TOUCH_Y + UNDO_H)) return false; if ( x > (UNDO_TOUCH_X + UNDO_W)) return false; if ( x < UNDO_TOUCH_X) return false; // Undo function int undoCol = gameRecord[--filledCellCount] - 1; // Subscript of column if (undoCol < 0) { filledCellCount++; return; } int undoRow; for (undoRow = R-1; undoRow > 0; undoRow--) if (game[undoRow][undoCol] > 0) break; displayStatus("Undo " + String(undoRow) + "," + String(undoCol)); game[undoRow][undoCol] = 0; eraseGamePiece(undoRow, undoCol); if (filledCellCount == 0) // Reset in case replay changes first piece played firstSelectedPiece = 0; return true; } boolean processRedo(int x, int y) { if ( y < REDO_TOUCH_Y) return false; if ( y > (REDO_TOUCH_Y + REDO_H)) return false; if ( x > (REDO_TOUCH_X + REDO_W)) return false; if ( x < REDO_TOUCH_X) return false; // Redo function int redoCol = gameRecord[filledCellCount]; // Column # for dropPiece (not subscript) displayStatus("Redo Col " + String(redoCol)); if ((redoCol < 1) || (redoCol > C)) return; // Next adapted from dropPiece() int c = redoCol - 1; // Subscript if (game[R-1][c] > 0) return; // Full column should not be possible here int r; // Compute lowest clear row for (r=0; r (AUTO_TOUCH_Y + AUTO_H)) return false; if ( x > (AUTO_TOUCH_X + AUTO_W)) return false; if ( x < AUTO_TOUCH_X) return false; if (autoplayInProgress) { autoplayInProgress = false; autoText = "Auto"; paintAutoButton(); } else { autoplayInProgress = true; autoText = "Stop"; paintAutoButton(); autoplayGame(); } return true; } boolean processClear(int x, int y) { // Clear displayed game if ( y < CLEAR_TOUCH_Y) return false; if ( y > (CLEAR_TOUCH_Y + CLEAR_H)) return false; if ( x > (CLEAR_TOUCH_X + CLEAR_W)) return false; if ( x < CLEAR_TOUCH_X) return false; // initGame(); if (filledCellCount > 0) { initGame(); displayStatus("Game cleared"); } else displayStatus("Clear button"); return true; } // Physical game (Robot arm) interface // Microswitch array NC = ground (LOW) NO = 8.1K to 3.3 volt pullup int getExternalPlay() { // Returns column number, not subscript // Skip XDIN[0] - Not a game array column int col; for (col=1; col 0) { // Debounce while (digitalRead(XDIN[col]) == HIGH) ; delay(READ_PAUSE); dropPiece(game, pieceToPlay(), col, true); if (!autoplayInProgress) // Externally initiated game replyToExternalPlay(); } } void replyToExternalPlay() { int col = selectPlay(); putPlayToRobotArm(col); dropPiece(game, pieceToPlay(), col, true); } void putPlayToRobotArm(int col) { if (col == 0) { // Reset all outputs LOW for (int i=0; i 7) // Parameter is column number, not subscript return; if (col % 2 == 1) // Convert to binary - BIT 0 digitalWrite(XOUT[2], HIGH); else digitalWrite(XOUT[2], LOW); if (col == 2 || col ==3 || col == 6 || col == 7) // BIT 1 digitalWrite(XOUT[1], HIGH); else digitalWrite(XOUT[1], LOW); if (col >= 4 ) // BIT 2 digitalWrite(XOUT[0], HIGH); else digitalWrite(XOUT[0], LOW); digitalWrite(XOUT[3], HIGH); // Data ready delay(READ_PAUSE); digitalWrite(XOUT[3], LOW); return; } // Informational void paintTestButton() { tft.fillRoundRect(TEST_X, TEST_Y, TEST_W, TEST_H, BUTTON_RADIUS, TEST_BACKGROUND_COLOR); tft.setCursor(TEST_X + 8, TEST_Y + 2); tft.setTextSize(TEST_TEXT_SIZE); tft.setTextColor(TEST_TEXT_COLOR); tft.print(TEST_TEXT); } boolean processTest(int x, int y) { if ( y < TEST_TOUCH_Y) return false; if ( y > (TEST_TOUCH_Y + TEST_H)) return false; if ( x > (TEST_TOUCH_X + TEST_W)) return false; if ( x < TEST_TOUCH_X) return false; // Test button flashLED(2); // Custom test functions below if (false) { displayStatus("Load OK .."); delay(TWOSEC); } if (true) if (testColumn > 0) { putPlayToRobotArm(testColumn); displayStatus("Col. " + String(testColumn) + " put to RA"); delay(TWOSEC); displayStatus("Clear output"); } else if (false) paintClearButton(); else if (false) testEval(); else if (false) testScore(); else if (false) testWin(); else if (false) displayStatus("isFull? " + String(isArrayFull(game))); else if (false) testMinimax(); else if (false) displayStatus("isWin? " + String(isWin(game))); else if (false) displayStatus("Scores: " + String(score(game, 1)) + ", " + String(score(game, 2))); else displayStatus("Test button"); // Placeholder return true; } void testMinimax() { // Can be invoked at any point in game (for either player) lookaheadDepth = 6; // Modify to test at different depths maxPlayerPiece = pieceToPlay(); minPlayerPiece = otherPiece(maxPlayerPiece); if (DEBUG_TEST) { Serial.println("maxPlayerPiece: " + String(maxPlayerPiece)); Serial.println("minPlayerPiece: " + String(minPlayerPiece)); printGameArray(game); // Before } copyGameArray(mmGame, game); // As argued to minimax ///* // Dynamic depth test lookaheadDepth = dynamicLookaheadDepth(); if (lookaheadDepth > MIN_DEPTH && filledCellCount < ENDGAME) displayStatus("mm: thinking"); //*/ // To do: Precheck mmGame for terminal node int mmScore = minimax(mmGame, lookaheadDepth, -INF, INF, true); ///* if (mmScore == -INF) { displayStatus("mm: Not good"); while(mmScore == -INF && (lookaheadDepth -= 2) > 0) mmScore = minimax(mmGame, lookaheadDepth, -INF, INF, true); } //*/ int col = mmSelectedColumn; String s; // Nicety if (mmScore == INF) s = "+INF"; else if (mmScore == -INF) s = "-INF"; else s = String(mmScore); // To do: Generalize to terminal node win/loss/tie, else play if (mmScore == -INF) displayStatus("mm: You win!"); else displayStatus("mm: " + String(col) + ", " + s); if (DEBUG_TEST) { printAlgebraicGameRecord(); Serial.println("mm: " + String(col) + ", " + s); } } void testWin() { // test function isWin(...) int testGame[R][C] = { {2, 1, 2, 1, 0, 1, 1}, {1, 2, 2, 2, 0, 0, 1}, {2, 2, 2, 1, 0, 0, 0}, {0, 2, 1, 1, 0, 0, 0}, {0, 1, 2, 1, 0, 0, 0}, {0, 0, 0, 2, 0, 0, 0} }; printGameArray(testGame); Serial.println("isWin(testGame) = " + String(isWin(testGame))); Serial.println(); } void testScore() { // test function isWin(...) int testGame[R][C] = { {1, 0, 0, 1, 0, 0, 0}, {2, 0, 0, 2, 0, 0, 0}, {1, 0, 0, 0, 0, 0, 0}, {2, 0, 0, 0, 0, 0, 0}, {1, 0, 0, 0, 0, 0, 0}, {2, 0, 0, 0, 0, 0, 0} }; printGameArray(testGame); Serial.println("Score for player 1 = " + String(score(testGame, 1))); Serial.println(); } void testEval() { int vv[4] = {1, 0, 0, 1}; Serial.println("evalFourVector = " + String(evalFourVector(vv, 1))); } void displayStatus(String s, uint16_t bkg) { tft.fillRect(STATUS_X, STATUS_Y, STATUS_W, STATUS_H, bkg); tft.setTextColor(STATUS_TEXT_COLOR); tft.setTextSize(STATUS_TEXT_SIZE); tft.setCursor(STATUS_X, STATUS_Y); tft.print(s); } void displayStatus(String s) { // Overload displayStatus(s, GAME_ARRAY_BACKGROUND); } // Debug // Teensy USB Serial void printGameArray(int g[R][C]) { char sym[3] = {'-', 'x', 'y'}; Serial.println(); for (int i=R-1; i>=0; i--) { for (int j=0; j