/* -*- mode:C++ -*- * * Sketch description: Scalable Tic Tac Toe v2 * * Sketch author: Dave */ #include "CDD.h" // For CDDStart, etc static void redisplay(); static void dumpBoard(u8 *); u32 endGameDisplaySteps = 0; u32 endGameDisplayContents = 0; bool endGameSides = false; #define REANALYZE_PERIOD 1000 // ms: Reanalyze and announce our state every this #define DISPLAY_STEP_PERIOD 50 // ms: How often the display program steps #define STALE_MODEL_DELAY 2200 // ms: World model info older than this should be scrubbed #define DRAW_WAIT 200 // ms: How long to let others see a win before declaring a draw enum Game { // Possible states of a game UNDETERMINED = 'a', X_TO_PLAY, O_TO_PLAY, X_WON, O_WON, DRAWN, MAX_GAME_VALUES }; enum Square { // State of a world model square UNHEARD = 'A', // Never heard from them (so their last is invalid) UNPLAYED, PLAYED_X, PLAYED_O, MAX_SQUARE_VALUES }; struct node { Point2D loc; u32 when; // Time we last heard from them u32 last; // Their most recent event number we've seen u8 square; // Contents of a square u8 game; // State of the game from their point of view void reset() { last = 0; when = 0; square = UNHEARD; game = UNDETERMINED; } }; #define HORIZON 2 // How far things extend away from me #define B_SIDE ((HORIZON)+1) // Game board side length, including me #define U_SIDE ((HORIZON)*2+1) // Side length of my universe node u[U_SIDE][U_SIDE]; // My universe #define ME (u[HORIZON][HORIZON]) #define N_OF_ME (u[HORIZON][HORIZON+1]) #define S_OF_ME (u[HORIZON][HORIZON-1]) #define E_OF_ME (u[HORIZON+1][HORIZON]) #define W_OF_ME (u[HORIZON-1][HORIZON]) #define NE_OF_ME (u[HORIZON+1][HORIZON+1]) #define SE_OF_ME (u[HORIZON+1][HORIZON-1]) #define SW_OF_ME (u[HORIZON-1][HORIZON-1]) #define NW_OF_ME (u[HORIZON-1][HORIZON+1]) node & getn(Point2D p) { u32 c = p.getX()+2; u32 r = p.getY()+2; API_ASSERT_MAX(c,U_SIDE); API_ASSERT_MAX(r,U_SIDE); return u[c][r]; } void initU() { for (int x = -HORIZON; x <= +HORIZON; ++x) { for (int y = -HORIZON; y <= +HORIZON; ++y) { const Point2D p(x,y); node & n = getn(p); n.loc = p; n.reset(); } } } static u32 analysisThread; static void reanalyzeAndAnnounce() { Alarms.set(analysisThread, millis()); } static u32 displayThread; static u32 displayPC = 0; static void redisplay() { displayPC = 0; Alarms.set(displayThread, millis()); } static u32 analyzeWin() { ///// // If I'm unplayed I see no win here if (ME.square != PLAYED_X && ME.square != PLAYED_O) return UNDETERMINED; ///// // Otherwise, am I the center square of a win? u32 win = ME.square == PLAYED_X?X_WON:O_WON; if (N_OF_ME.square == ME.square && S_OF_ME.square == ME.square) return win; if (E_OF_ME.square == ME.square && W_OF_ME.square == ME.square) return win; if (NE_OF_ME.square == ME.square && SW_OF_ME.square == ME.square) return win; if (NW_OF_ME.square == ME.square && SE_OF_ME.square == ME.square) return win; return UNDETERMINED; } static u32 analyzeBoard() { static u32 drawTimeout = 0; // Previously noticed draw possibility? if (drawTimeout != 0 && IS_EARLIER(drawTimeout,millis())) { // Win-wait time expired? drawTimeout = 0; // Yes, clear drawTimer return DRAWN; // and declare a draw } u32 xs = 0, os = 0, opens = 0, inactives = 0; u32 ngs = UNDETERMINED; for (int i = 0; i < U_SIDE; ++i) { for (int j = 0; j < U_SIDE; ++j) { node & n = u[i][j]; if (IS_EARLIER(n.when+STALE_MODEL_DELAY,millis())) n.reset(); switch (n.square) { case UNPLAYED: ++opens; break; case PLAYED_X: ++xs; break; case PLAYED_O: ++os; break; case UNHEARD: ++inactives; break; } switch (n.game) { case DRAWN: if (ngs == UNDETERMINED) // Prefer to take part in a win. ngs = DRAWN; break; case X_WON: case O_WON: ngs = n.game; break; } } } if (ngs != UNDETERMINED) { endGameDisplaySteps = 14; endGameSides = true; if (ngs == X_WON) endGameDisplayContents = PLAYED_X; else if (ngs == O_WON) endGameDisplayContents = PLAYED_O; else { endGameDisplayContents = ME.square; endGameSides = false; } ME.square = UNPLAYED; redisplay(); return X_TO_PLAY; } if (opens==0) { if (drawTimeout == 0) { // Just noticing draw possibility? drawTimeout = millis()+DRAW_WAIT; // Yes, set timer. if (drawTimeout == 0) ++drawTimeout; // Skip timer unset special case } return UNDETERMINED; } if (xs <= os) return X_TO_PLAY; else return O_TO_PLAY; } static u32 eventNum = MAP4BY8('0','0','0','0'); // Start with something parseable, wtfnot u32 lastEventNum() { return eventNum; } u32 nextEventNum() { return ++eventNum; } const Point2D ORIGIN(0,0); u32 mandist(const Point2D a, const Point2D b = ORIGIN) { s16 dx = a.getX()-b.getX(); if (dx < 0) dx = -dx; s16 dy = a.getY()-b.getY(); if (dy < 0) dy = -dy; return (dx > dy)?dx:dy; } void announceState(node & n, u32 skipFace = FACE_COUNT) { if (n.square == UNHEARD) return; Point2D scratch; for (u32 f = NORTH; f <= WEST; ++f) { if (f == skipFace || !CDDMapIn(f,ORIGIN,scratch)) continue; facePrintf(f, "e%l%h%h%c%c\n", n.last, n.loc.getX(),n.loc.getY(), n.square, n.game); } } /* Packet handler on 'e' Event announcements Format: e + + + + + Examples: e32(0,0)s3 (event num 32 from me: my 's'tatus is 3) */ /* on 'e' */ void handleAnnouncement(u8 * packet) { ///// // Parse it u32 theirEventNum; s16 theirX, theirY; Point2D theirCsrc, theirC; u8 theirSquare, theirGame; if (packetScanf(packet,"e%l%h%h%c%c\n", &theirEventNum,&theirX,&theirY, &theirSquare,&theirGame) != 7) { pprintf("L bad '%#p'\n",packet); return; } theirCsrc.setX(theirX); theirCsrc.setY(theirY); if (!CDDMapIn(packetSource(packet), theirCsrc, theirC)) { pprintf("L no map from %c, dropping '%#p'\n", FACE_CODE(packetSource(packet)),packet); return; } ///// // Extraterrestrials are not real u32 dist = mandist(theirC); if (dist > HORIZON) return; ///// // Stop me if you've heard this one node & n = getn(theirC); if (n.square != UNHEARD && IS_LATER_OR_EQUAL(n.last,theirEventNum)) return; ///// // Repropagate and analyze n.last = theirEventNum; n.when = millis(); if (n.square != theirSquare || n.game != theirGame) { // Is their state changing? n.square = theirSquare; n.game = theirGame; dumpBoard(0); reanalyzeAndAnnounce(); } announceState(n,packetSource(packet)); } void reboot(u8 *) { reenterBootloader(); } void show(u32 state) { ledOff(BODY_RGB_RED_PIN); ledOff(BODY_RGB_GREEN_PIN); ledOff(BODY_RGB_BLUE_PIN); switch (state) { case UNPLAYED: break; case PLAYED_X: ledOn(BODY_RGB_GREEN_PIN); break; case PLAYED_O: ledOn(BODY_RGB_RED_PIN); break; case 0: break; default: ledOn(BODY_RGB_BLUE_PIN); ledOn(BODY_RGB_RED_PIN); break; } } static void sideLEDs(bool on) { for (u32 f = NORTH; f <= WEST; ++f) ledSet(pinInFace(FACE_LED_PIN,f), on); } /* Display handler runs at 20Hz nominal, or slightly higher because of phase jumping. It displays the state color interrupted by 50ms color-to-play or black 'antiheartbeats' every ~1.5s. */ void displayThreadRun(u32 when) { if (endGameDisplaySteps > 0) { bool off = endGameDisplaySteps&1; if (off) { show(0); sideLEDs(false); } else { show(endGameDisplayContents); sideLEDs(endGameSides); } --endGameDisplaySteps; } else { if ((displayPC++&0x1f)==0 && ME.square==UNPLAYED) show(ME.game==X_TO_PLAY?PLAYED_X:PLAYED_O); else show(ME.square); } Alarms.set(displayThread, when+DISPLAY_STEP_PERIOD); } void analysisThreadRun(u32 when) { ME.when = when; u32 oldStance = ME.game; u32 s = analyzeWin(); if (s == UNDETERMINED) s = analyzeBoard(); if (s != UNDETERMINED) ME.game = s; ME.last = nextEventNum(); announceState(ME); if (ME.game != oldStance) redisplay(); Alarms.set(analysisThread, when+REANALYZE_PERIOD); } void dumpBoard(u8 * packet) { for (int y = HORIZON; y >= -HORIZON; --y) { if (packet) print(&packet[1],packetLength(packet)-1); // Insert routing prefix print("L"); for (int x = -HORIZON; x <= +HORIZON; ++x) { const Point2D p(x,y); node & n = getn(p); print(" "); switch (n.game) { default: print("."); break; case DRAWN: print("d"); break; case X_WON: print("X"); break; case O_WON: print("O"); break; case X_TO_PLAY: print("x"); break; case O_TO_PLAY: print("o"); break; } switch (n.square) { case UNHEARD: print("."); break; case UNPLAYED: print("_"); break; case PLAYED_X: print("X"); break; case PLAYED_O: print("O"); break; default: print(n.square,DEC); break; } } println(); } } void resetBoard(u8 *) { initU(); } void setup() { SET_REFLEX_FLAGS(RF_DIE_ALOUD); // Customize the physics for (u32 f = NORTH; f <= WEST; ++f) // Claim the face LEDs for ourselves ledOff(pinInFace(FACE_LED_PIN,f)); initU(); // Now create the universe Alarms.set(displayThread = Alarms.create(displayThreadRun),0); Alarms.set(analysisThread = Alarms.create(analysisThreadRun),0); ME.square = UNPLAYED; // We're in reset state Body.reflex('e',handleAnnouncement); Body.reflex('b',reboot); Body.reflex('d',dumpBoard); Body.reflex('r',resetBoard); CDDStart(); // Start up CDD system setLogFace(ALL_FACES); // Spew our log messages in all directions } void loop() { if (ME.square==UNPLAYED && buttonDown()) { ME.square = ME.game==X_TO_PLAY?PLAYED_X:PLAYED_O; ME.game = UNDETERMINED; reanalyzeAndAnnounce(); while (buttonDown()) delay(10); } } #define SFB_SKETCH_CREATOR_ID B36_4(a,h,a,x) #define SFB_SKETCH_PROGRAM_ID B36_4(s,t,t,t) #define SFB_SKETCH_COPYRIGHT_NOTICE "2009 David Ackley Placed in the public domain"