Detecting a victory condition across multiple Blinks

While playing Fracture, I wondered if it would be possible to detect an “end of game” / victory condition across all connected Blinks.

One possible strategy would be for Blinks to transmit a “not done” status message every second or so, maybe with a hop count attached.

When a Blink has not received a “not done” message for longer than the transmit interval, it can switch to an “end of game” state.

A challenge would be relaying the “not done” message to other non-contiguous Blinks. It might be possible to do this by having every Blink that receives a “not done” retransmit it, on every side except the one that it received it on. Would need to ensure that loops don’t occur. Perhaps by serializing “not done” messages and making sure that a message that has already been relayed is not relayed again by the same Blink?

If each Blink had a unique ID this might be easier. “Not done” messages could be tagged with their sender and maybe a hop/retransmit count.

This seems like a generalizable situation, ensuring that all nodes share the same state.

Any thoughts?

1 Like

I think this is basically a question of whether showing victory/end-game should be in the API code, versus in each game sketch. Currently I believe it’s up to each individual game to handle both victory conditions as well as the inevitable “restarting” of the game after game over. (The latter is being discussed over here: https://github.com/Move38/Move38-Arduino-Platform/issues/45)

It sounds to me like you are advocating for the API handling showing the end-game state. I can see pluses and minuses to this approach. (I’m currently writing some “end of game” code myself.)

Pluses:

  • Usability would probably really benefit from some conventions. Showing the game is over consistently is one of them.
  • The more code that moves into the API the easier it will be to write for external developers (like myself).
  • Similar to usability conventions benefiting the users, coding conventions will benefit developers.
  • There is already precedent for this kind of “API level” state change, what with 6+ second presses turning the blinks into programming/sharing mode, and longer presses putting the blinks to sleep.

Minuses:

  • Some games might not have a “game is over” state (or will want to show the game is over in a unique way).
  • The more special cases like this exist, the more important the developers understanding what these conventions are, because we will have to program to them. Basically, documentation becomes harder (and more important).

Before writing these down, I was leaning toward sketch/developer-code having this responsibility, but now that I’ve made the list, it does seem like there are significant advantages to the API handling it.

1 Like

For The Cloud Game, the cluster of Blinks needs to determine if all Blinks are illuminated or in the cloud state. Conversely, a single Blink that is off immediately returns the fact that this is not a win condition. Fracture could use a similar technique but be specific to to color.

Below is an algorithm I wrote that on each compute cycle (or more accurately, I’ve slowed down the compute cycles to 50ms, feel free to play with this as it is nice to see how the Blinks think), continues to find neighbors that have not yet been searched, and if the neighbor confirms a no-win, it will halt and return that news up the chain to then be spread to the entire population, or if it covers all connected Blinks, and returns, it will spread the news of the win to the entire population.

single click = turn a Blink on or off
double click = start the search for a win

The problem with this current method is that it is not safe from another search being started before a single search has ended. What do you think is the best way to handle this gracefully. The current search algorithm uses 7 distinct messages (5 for the search and 2 for win or lose condition). It would be great to not use more than 8 distinct messages because then it only uses 3 bits in parallel communication.

Here is a video of the algorithm in action and with two examples of failures from two searches being started around the same time.

Note: Win is represented by MAGENTA and No-win found is represented by ORANGE

The following also runs nicely in the simulator to quickly try it out.

/*
   Search for any "OFF" Blinks, if none found, signal win condition

   How to use:
    Single Click = turn a Blink on or off (toggle)
    Double Click = start search from this Blink
*/

enum winSearchValues {CHILL, SEARCHING, WAITING, FOUND_OFF, NO_FOUND_OFF, VICTORY, DEFEAT};

bool isSearchingForWin;
bool isWaitingOnNeighbor;
bool foundWin = false;
byte neighborSearchingForWin;    //changing index
byte indexOfNeighborToReportTo = 6;  // use this for where we started our search

bool isOn = true;
bool flashOn = false;

byte faceValues[6] = {CHILL, CHILL, CHILL, CHILL, CHILL, CHILL};


Timer slowTimer;
#define FRAME_DELAY 50

Timer flashTimer;
#define FLASH_DELAY 100

void setup() {


}

void loop() {

  if (slowTimer.isExpired()) {
    slowTimer.set(FRAME_DELAY);

    if (buttonSingleClicked()) {
      // I am one that was clicked
      isOn = !isOn;

    }
    if (buttonDoubleClicked()) {

      //Start Search for any off blinks
      //If I am an off blink, no need to search
      if (!isOn) {
        isSearchingForWin = false;
      }
      else {
        //Else Ask each neighbor to search for off blinks
        isSearchingForWin = true;
        setAllTo(SEARCHING);
        neighborSearchingForWin = 0;
        indexOfNeighborToReportTo = 6;  //special for master blink
      }

    }

    if (isSearchingForWin) {

      //check if done
      if (isDoneSearching()) {
        isSearchingForWin = false;
        if (indexOfNeighborToReportTo == 6) { // if I am the origin
          foundWin = true;
          setAllTo(VICTORY);
        }
      }

      if (isValueReceivedOnFaceExpired(neighborSearchingForWin)) { // no neighbor!
        faceValues[neighborSearchingForWin] = NO_FOUND_OFF;
        neighborSearchingForWin = (neighborSearchingForWin + 1) % 6;
      }
      else { //found neighbor!
        if (neighborSearchingForWin == indexOfNeighborToReportTo) {
          faceValues[neighborSearchingForWin] = NO_FOUND_OFF;
          faceValues[indexOfNeighborToReportTo] = NO_FOUND_OFF;
          isSearchingForWin = false;
        }
        else {

          byte neighborValue = getLastValueReceivedOnFace(neighborSearchingForWin);

          if (!isWaitingOnNeighbor) { // if neighbor has not yet been asked to search

            if (neighborValue == CHILL) {
              faceValues[neighborSearchingForWin] = WAITING;
              isWaitingOnNeighbor = true;
            }
            else if (neighborValue == SEARCHING ) {
              //ignore and move on to the next one
              faceValues[neighborSearchingForWin] = NO_FOUND_OFF;
              neighborSearchingForWin = (neighborSearchingForWin + 1) % 6;  // continue looking to the next one
            }
            else if (neighborValue == FOUND_OFF) {
              isSearchingForWin = false;
              neighborSearchingForWin = 0;
              faceValues[indexOfNeighborToReportTo] = FOUND_OFF;
            }
            else if (neighborValue == NO_FOUND_OFF) {
              //Ask neighbor to search for off blinks
              faceValues[neighborSearchingForWin] = NO_FOUND_OFF;
              neighborSearchingForWin = (neighborSearchingForWin + 1) % 6;  // continue looking to the next one
            }

          } // end not waiting on neighbor
          else {  // am waiting on neighor

            // let the one I am reporting to know I am waiting on a neighbor
            if (indexOfNeighborToReportTo != 6) { // if I have someone to report to
              faceValues[indexOfNeighborToReportTo] = WAITING;
            }

            if (neighborValue == FOUND_OFF) { // just heard back from neighbor that they FOUND AN OFF
              faceValues[neighborSearchingForWin] = FOUND_OFF;
              isSearchingForWin = false;
              isWaitingOnNeighbor = false;
              neighborSearchingForWin = 0;
              faceValues[indexOfNeighborToReportTo] = FOUND_OFF;
              // if I am the starting point..
              // signal no victory, stop the search, cuz we found an off
              if (indexOfNeighborToReportTo == 6) {
                setAllTo(DEFEAT);
              }
            }
            else if (neighborValue == NO_FOUND_OFF) { // just heard back from neighbor that they haven't found and OFF
              // Acknowledge and move on to the next one
              faceValues[neighborSearchingForWin] = NO_FOUND_OFF;
              isWaitingOnNeighbor = false;
              // if I am the starting point..
              // signal victory, cuz we searched all and didn't find an OFF

              if (indexOfNeighborToReportTo != 6) { // I'm not the starting point
                neighborSearchingForWin = (neighborSearchingForWin + 1) % 6;
              }
              else { // I'm the starting point
                if (neighborSearchingForWin == 5) { // last side I will search
                  // signal victory, cuz we searched all and didn't find an OFF
                  isSearchingForWin = false;
                  foundWin = true;
                  setAllTo(VICTORY);
                }
                else {
                  // advance to the next side, if we have search all sides, stop searching
                  neighborSearchingForWin = (neighborSearchingForWin + 1) % 6;
                }
              }
            }
          } //end waiting on neighbor
        }
      }
    }
    else // not yet searching, listening for the opportunity to participate
    {
      // look to present neighbors
      FOREACH_FACE(f) {
        if (!isValueReceivedOnFaceExpired(f)) {
          byte neighborValue = getLastValueReceivedOnFace(f);

          if (neighborValue == VICTORY) {
            setAllTo(VICTORY);  // spread the victory
          }
          else if (neighborValue == DEFEAT) {
            setAllTo(DEFEAT);  // spread the victory
          }

          if (neighborValue == WAITING && f != indexOfNeighborToReportTo) { //if neighbor face value is searching

            // set all to searching
            setAllTo(SEARCHING);

            if (!isOn) { //if off
              faceValues[f] = FOUND_OFF;
              setAllTo(FOUND_OFF);
            }
            else { //if on
              // I've been asked to search
              indexOfNeighborToReportTo = f;
              faceValues[f] = WAITING;
              neighborSearchingForWin = (f + 1) % 6; //move clockwise on faces
              isSearchingForWin = true;
            }

          } // end if neighbor is searching

        }// end if neighbor is present


      } // end for loop
    } // end not yet searching

  } // end slow timer
  FOREACH_FACE(f) {
    setValueSentOnFace(faceValues[f], f);
  }

  // Listen for message from neighbor to search for any off blinks
  // if I am off,
  // return message to neighbor asking me to search; message should say "i am an off blink"
  // else if any of my neighbors are not yet searched blinks
  // then ask them to search for any off blinks


  if (isOn) {
    setColor(WHITE);
  }
  else {
    setColor(dim(BLUE, 64));
  }

  // DEBUG VISUALIZATION
  FOREACH_FACE(f) {
    switch (faceValues[f]) {
      case CHILL:
        {
          if (isOn) {
            setColorOnFace(WHITE, f);
          }
          else {
            setColorOnFace(dim(BLUE, 64), f);
          }

        }
        break;
      case SEARCHING:
        setColorOnFace(BLUE, f);
        break;
      case WAITING:
        setColorOnFace(YELLOW, f);
        break;
      case FOUND_OFF:
        setColorOnFace(RED, f);
        break;
      case NO_FOUND_OFF:
        setColorOnFace(GREEN, f);
        break;
      case DEFEAT:
        setColorOnFace(ORANGE, f);
        break;
      case VICTORY:
        setColorOnFace(MAGENTA, f);
        break;
    }
  } // end face loop

  if (flashTimer.isExpired()) {
    flashOn = !flashOn;
    flashTimer.set(FLASH_DELAY);

  }
  if (flashOn && isSearchingForWin) {
    setColorOnFace(OFF, neighborSearchingForWin);
  }
  //END DEBUG VISUALIZATION
}

void setAllTo(byte state) {
  FOREACH_FACE(f) {
    faceValues[f] = state;
  }
}

bool isDoneSearching() {
  FOREACH_FACE(f) {
    if (faceValues[f] == CHILL || faceValues[f] == WAITING || faceValues[f] == SEARCHING) {
      return false;
    }
  }
  return true;
}

One interesting problem to solve for Blinks revolves around this: How to make all Blinks react to a state but only if all Blinks in the cluster reach that state?

I gave a lot of though about trying to solve this with face values but the solutions were always complicated and full of corner cases.

In the end, there is a simpler way (code-wise) to implement this by using my broadcast library. Here is the general idea (I may implement it later):

1 - Whenever a blink is double clicked, send a message to all Blinks (trivial with the library). Say MESSAGE_IS_ON.
2 - The message automatically propagates to all Blinks and when it reaches a “leaf” Blink, that Blink checks its status and send back a reply with a payload of 0 (if it is OFF) or 1 if it is ON.
3 - The library percolates this down to all Blinks until each reaches the one that sent the original message. For every Blink it goes trough, the Blink will set the payload to 1 if all Blinks connected to it reported 1 and its own state is on or 0 otherwise.
4 - The reply that reaches the original Blink will either have a payload of 0 (at least one Blink reported off) or 1 (all, Blinks reported on).
5 - Upon receiving the reply, if the payload is 1, send a fire-and-forget message (one that does not have a reply) to all Blinks saying the game is over.

The absolute majority of what is done above happens automatically.

This is exactly what Hexenwood used to do before I implemented a local cluster map on each Blink to keep track of the current state.

1 Like