Connect Fourduino

Welcome to my first project!

I wanted to post before final completion to get some feedback, ideas, suggestions, etc...

I've designed (with lots of help from the forums and other projects around the net) an electronic 'Connect Four' clone. It consists of 84 Led's (half green/red) at the moment, but when funds are less tight it will be redesigned with 42 RGB's instead. It is pretty much just like the original. Players take turns dropping 'chips' in one of 7 columns trying to get 4 in a row, horizontally, vertically or diagonally.

I used one 74hc595 per colour to drive the columns, and a uln2803 for the rows. I would prefer to use the tpic6b595 instead of the 75hc's but I cannot find them locally and have no interest in paying large shipping fees for a couple of chips...

I fully intend to release the project as open source, and assuming there is interest provide kits.

Below is the (current) schematic of the control board, the display is just a simple anode column, common cathode row RG matrix. I've also included a few pictures of the current prototype (still on the breadboard actually).

I have also provided the code, below the pictures.

If anyone has any suggestions or see's any problems with my current circuit, please let me know :)

Thanks for reading, I will keep this thread updated as progress is made.

Control Board Schematic.

/*
TODO;
Flash winning chips
Intro Mode (automatically drop chips in both colours until a start button is pressed.)
Find appropriate attributions for inspriation/borrowed code.
??Scrolling Text??
*/

#define DEBUG 0
#define BENCH 0

// Pins of CD4021BE
#define latchPin4021 15 // analog 1
#define dataPin4021 16 // analog 2
#define clockPin4021 14 // analog 0

// Pins on 74HC595
#define latchPin595 11
#define clockPin595 10
#define dataPin595 9

#define piezo 8 // piezo pin

// Button Pins on CD4021BE
#define SW_COL7 6
#define SW_COL6 5
#define SW_COL5 4
#define SW_COL4 3
#define SW_COL3 2
#define SW_COL2 1
#define SW_COL1 0

// status led pins
#define STATUS_RED 18
#define STATUS_GRN 17
#define STATUS_BLU 19

#define dropTime 50 // speed of dropping 'chips'
#define MAX_COLS 7 // Columns of Matrix
#define MAX_ROWS 6 // Rows of Matrix
#define REDPLAYER 0 // Index of red player led state
#define GRNPLAYER 1 // Index of green player led state

byte pinForRow[6] = {2, 3, 4, 5, 6, 7}; // Pins for rows (through ULN2803A)
byte buttonList[7] = {SW_COL1, SW_COL2, SW_COL3, SW_COL4, SW_COL5, SW_COL6, SW_COL7};

byte buttons = B1111111; // Default state of buttons
byte lastButtonState = buttons;

byte softPrescaler = 0; // not too sure :)
byte refreshRate = 15;
byte activeRow = 0;

unsigned long last = 0; // for debouncing buttons
unsigned long before = 0; // for debugging/benchmarking

boolean curPlayer = REDPLAYER; // currently active player
boolean easterEgg = false; // are we showing the easter egg? ;)

byte ledState[2][MAX_ROWS] = { // Current State of Matrix
  {B0000000,  // Red State
   B0000000,
   B0000000,
   B0000000,
   B0000000,
   B0000000},

  {B0000000, // Green State
   B0000000,
   B0000000,
   B0000000,
   B0000000,
   B0000000}   
};

void setup() {
  // Calculation for timer 2
  // 16 MHz / 8 = 2 MHz (prescaler 8)
  // 2 MHz / 256 = 7812 Hz
  // soft_prescaler = 15 ==> 520.8 updates per second
  // 520.8 / 8 rows ==> 65.1 Hz for the complete display
  TCCR2A = 0;           // normal operation
  TCCR2B = (1<<CS21);   // prescaler 8
  TIMSK2 = (1<<TOIE2);  // enable overflow interrupt

  //Start Serial for debuging purposes      
  Serial.begin(9600);

  pinMode(latchPin595, OUTPUT);
  pinMode(clockPin595, OUTPUT);
  pinMode(dataPin595, OUTPUT);

  pinMode(latchPin4021, OUTPUT);
  pinMode(clockPin4021, OUTPUT);
  pinMode(dataPin4021, INPUT);

  pinMode(piezo, OUTPUT);
  
  pinMode(STATUS_RED, OUTPUT);
  pinMode(STATUS_GRN, OUTPUT);
  pinMode(STATUS_BLU, OUTPUT);

  for (int i=0; i<6; i++) pinMode(pinForRow[i], OUTPUT);
  
  digitalWrite(STATUS_RED, LOW);
  digitalWrite(STATUS_GRN, HIGH);
  digitalWrite(STATUS_BLU, HIGH);
}

ISR(TIMER2_OVF_vect) {
  softPrescaler++;
  if (softPrescaler == refreshRate) {
    displayActiveRow();
    softPrescaler = 0;
  }
};

void displayActiveRow() {
  // disable current row;
  digitalWrite(pinForRow[activeRow], LOW);

  // set next row;
  activeRow = (activeRow + 1) % MAX_ROWS;

  // shift out values for this row;
  digitalWrite(latchPin595, LOW);
  if (easterEgg) {
    shiftOut(dataPin595, clockPin595, MSBFIRST, random(128));
    shiftOut(dataPin595, clockPin595, MSBFIRST, random(128));
  } else {
    shiftOut(dataPin595, clockPin595, MSBFIRST, ledState[REDPLAYER][activeRow]);
    shiftOut(dataPin595, clockPin595, MSBFIRST, ledState[GRNPLAYER][activeRow]);
  }
  digitalWrite(latchPin595, HIGH);

  // switch to new row;
  digitalWrite(pinForRow[activeRow], HIGH);
}

void loop() {
  byte button;
  byte finalRow;
  
  buttons = shiftIn(dataPin4021, clockPin4021);
  if ((buttons != lastButtonState) && (micros() - last > 50)) {
    if (buttons == B101010) {
      //randomSeed(analogRead(0));
      byte tmp = refreshRate;
      byte tmp2 = PORTC ; // save state of status led
      refreshRate = 100;
      easterEgg = true;
      while ((shiftIn(dataPin4021, clockPin4021)) == B101010) {
        digitalWrite(STATUS_RED, LOW);// very kludgy, and very temporary
        digitalWrite(STATUS_GRN, HIGH);
        digitalWrite(STATUS_BLU, HIGH);
        delay(refreshRate);
        digitalWrite(STATUS_RED, LOW);
        digitalWrite(STATUS_GRN, LOW);
        digitalWrite(STATUS_BLU, HIGH);
        delay(refreshRate);
        digitalWrite(STATUS_RED, LOW);
        digitalWrite(STATUS_GRN, LOW);
        digitalWrite(STATUS_BLU, LOW);
        delay(refreshRate);
        digitalWrite(STATUS_RED, LOW);
        digitalWrite(STATUS_GRN, HIGH);
        digitalWrite(STATUS_BLU, HIGH);
        delay(refreshRate);
        digitalWrite(STATUS_RED, HIGH);
        digitalWrite(STATUS_GRN, LOW);
        digitalWrite(STATUS_BLU, HIGH);
        delay(refreshRate);
        digitalWrite(STATUS_RED, HIGH);
        digitalWrite(STATUS_GRN, HIGH);
        digitalWrite(STATUS_BLU, LOW);
        delay(refreshRate);
      }
      PORTC = tmp2; // return status 
      easterEgg = false;
      refreshRate = tmp;
      return;
    }
    last = micros();

    for (int col = 0;col < MAX_COLS;col++) {
      if (!getBit(buttons, buttonList[col])) {
        finalRow = dropChip(buttonList[col]);
        if (BENCH) before = millis();
        if (checkForWin(ledState[curPlayer], finalRow, col)) {
          if (BENCH) {Serial.print("Execution Time: ");Serial.println(millis()-before);}
          playWinTones();
          resetGame();
          if (DEBUG) {Serial.println("WINNER FOUND! :D");}
        } else {
          swapPlayers();
          if (DEBUG) {Serial.println("No Winner :(");}
        }
      }
    }
  }
  lastButtonState = buttons;
}

/************************************************************************************/

void swapPlayers() {
  curPlayer = !curPlayer;
  if (curPlayer == REDPLAYER) {
    digitalWrite(STATUS_RED, LOW);
    digitalWrite(STATUS_GRN, HIGH);  
  } else {
    digitalWrite(STATUS_RED, HIGH);
    digitalWrite(STATUS_GRN, LOW);  
  }
}

void ledOn(byte player, int x, int y) {
  ledState[player][y] |= 1 << x;
}

void ledOff(byte player, int x, int y) {
  ledState[player][y] &= ~(1 << x);
}

boolean getBit(byte myVarIn, byte whatBit) {
  return (myVarIn & (1 << whatBit));
}

boolean isLedOn(int x, int y) {
  return (ledState[REDPLAYER][y] & (1 << x)) || (ledState[GRNPLAYER][y] & (1 << x));
}

byte dropChip(int col) {
  byte finalRow;

  if (DEBUG) {Serial.print("Dropping Column #");Serial.println(col);Serial.print("Current Player: ");Serial.println(curPlayer, BIN);}

  if (!isLedOn(col, 0)) ledOn(curPlayer, col, 0);
  delay(dropTime);
  for (int r = 1;r < MAX_ROWS;r++) {
    finalRow = r;
    if (!isLedOn(col, r)) {
      ledOff(curPlayer, col, r-1);
      ledOn(curPlayer, col, r);
    } else {finalRow--;break;}
    delay(dropTime);
  }

  if (DEBUG) Serial.print("Final Row: ");
  if (DEBUG) Serial.println(finalRow, DEC);
  return finalRow;
}

void resetGame() {
  for (int i=0;i < MAX_ROWS;i++) {
    ledState[REDPLAYER][i] = B0000000;
    ledState[GRNPLAYER][i] = B0000000;
  }
  curPlayer = REDPLAYER;
  digitalWrite(STATUS_RED, LOW);
  digitalWrite(STATUS_GRN, HIGH);  
}

void flashWinningChips() {}
boolean checkForWin(byte* gameState, int row, int col) {
  if (DEBUG) {Serial.print("Checking FROM: ");Serial.print(row);Serial.print(" x ");Serial.println(col);}

  if (checkHorizontal(gameState, row, col)) return true;
  else if (checkVertical(gameState, row, col)) return true;
  else if (checkLeftDiagonal(gameState, row, col)) return true;
  else if (checkRightDiagonal(gameState, row, col)) return true;
  return false;
}

boolean checkHorizontal(byte* gameState, int row, int col) {
  int cnt = 1;

  for (int c= col-1; c >= 0;c--) { // check to the left
    if (DEBUG) {
      Serial.print("Checking column: ");
      Serial.println(c);
    }
    if (gameState[row] & 1 << c) {
      if (DEBUG) Serial.print("Player Piece Found");
      cnt++;
    } else break;
  }

  for (int c = col+1; c < MAX_COLS;c++) {
    if (DEBUG) {
      Serial.print("Checking column: ");
      Serial.println(c);
    }
    if (gameState[row] & 1 << c) {
      if (DEBUG) Serial.println("Player Piece Found");
      cnt++;
    } else break;
  }

  if (cnt >= 4) return true;
  return false;
}

boolean checkVertical(byte* gameState, int row, int col) {
  int cnt = 1;

  if (row < MAX_ROWS - 3) {
    for (int i = row+1; i <= row + 3; i++) {
      if (gameState[i] & 1 << col) cnt++;
    }
  }

  if (cnt >= 4) return true;
  return false;
}  

boolean checkLeftDiagonal (byte* gameState, int row, int col) {
  int cnt = 1;
  int r = row-1;
  int c = col-1;

  while (r >= 0 && c >= 0) {
    if (DEBUG) {
      Serial.print("Checking Row: ");
      Serial.print(r);
      Serial.print(" Column: ");
      Serial.println(c);
    }
    if (gameState[r] & 1 << c) {
      cnt++;
      r--;
      c--;
      if (DEBUG) {
        Serial.print("Player Piece Found, Counter: ");
        Serial.println(cnt);
      }
    } else break;
  }
  
  r = row+1;
  c = col+1;
  while (r < MAX_ROWS && c < MAX_COLS) {
    if (DEBUG) {
      Serial.print("Checking Row: ");
      Serial.print(r);
      Serial.print(" Column: ");
      Serial.println(c);
    }
    if (gameState[r] & 1 << c) {
      cnt++;
      r++;
      c++;
      if (DEBUG) {
        Serial.print("Player Piece Found, Counter: ");
        Serial.println(cnt);
      }
    } else break;
  }

  if (cnt >= 4) return true;
  return false;
}

boolean checkRightDiagonal (byte* gameState, int row, int col) {
  int cnt = 1;
  int r = row + 1;
  int c = col - 1;
  while (r < MAX_ROWS && c >= 0) {
    if (DEBUG) {
      Serial.print("Checking Row: ");
      Serial.print(r);
      Serial.print(" Column: ");
      Serial.println(c);
    }
    if (gameState[r] & 1 << c) {
      cnt++;
      r++;
      c--;
      if (DEBUG) {
        Serial.print("Player Piece Found, Counter: ");
        Serial.println(cnt);
      }
    } else break;
  }
  
  r = row-1;
  c = col+1;
  while (r >= 0 && c < MAX_COLS) {
    if (DEBUG) {
      Serial.print("Checking Row: ");
      Serial.print(r);
      Serial.print(" Column: ");
      Serial.println(c);
    }
    if (gameState[r] & 1 << c) {
      cnt++;
      r--;
      c++;
      if (DEBUG) {
        Serial.print("Player Piece Found, Counter: ");
        Serial.println(cnt);
      }
    } else break;
  }

  if (cnt >= 4) return true;
  return false;
}

void playWinTones(){
//  cli();
  buzz(piezo, 523, 1000/6); // C 1/8
  buzz(piezo, 523, 1000/6); // C 1/8
  buzz(piezo, 523, 1000/6); // C 1/8
  buzz(piezo, 523, 1000/3); // C 1/4
  buzz(piezo, 415, 1000/3); // A-Flat 1/4
  buzz(piezo, 466, 1000/3); // B-Flat 1/4
  buzz(piezo, 523, 1000/6); // C 1/8
  delay(25); // trial and error 1/8
  buzz(piezo, 466, 1000/6); // B-Flat 1/8
  buzz(piezo, 523, 1000/1.5); // C 1/2.
//  sei();
}

void buzz(int targetPin, long frequency, long length) {
    long delayValue = 1000000/frequency/2; // calculate the delay value between transitions
    // 1 second's worth of microseconds, divided by the frequency, then split in half since
    // there are two phases to each cycle
    long numCycles = frequency * length/ 1000; // calculate the number of cycles for proper timing
    // multiply frequency, which is really cycles per second, by the number of seconds to 
    // get the total number of cycles to produce
    for (long i=0; i < numCycles; i++){ // for the calculated length of time...
        digitalWrite(targetPin,HIGH); // write the buzzer pin high to push out the diaphram
        delayMicroseconds(delayValue); // wait for the calculated delay value
        digitalWrite(targetPin,LOW); // write the buzzer pin low to pull back the diaphram
        delayMicroseconds(delayValue); // wait againf or the calculated delay value
    }
    delayMicroseconds(delayValue*5);
}

byte shiftIn(int myDataPin, int myClockPin) { 
  // internal function setup
  int i;
  int temp = 0;
  byte myDataIn = 0;

  //Pulse the latch pin: set it to 1 to collect parallel data
  digitalWrite(latchPin4021,1);

  //set it to 1 to collect parallel data, wait
  delayMicroseconds(20);

  // set it to 0 to transmit data serially  
  digitalWrite(latchPin4021,0);

  // we will be holding the clock pin high 8 times (0,..,7) at the
  // end of each time through the for loop
  // at the begining of each loop when we set the clock low, it will
  // be doing the necessary low to high drop to cause the shift
  // register's DataPin to change state based on the value
  // of the next bit in its serial information flow.
  // The register transmits the information about the pins from pin 7 to pin 0 
  // so that is why our function counts down
  for (i = 7;i >= 0;i--) {
    digitalWrite(myClockPin, LOW); 
    delayMicroseconds(0.2);
    temp = digitalRead(myDataPin);
    if (temp) {
      myDataIn = myDataIn | (1 << i); // set the bit to 0 no matter what
    }
    digitalWrite(myClockPin, HIGH);
  }
  return myDataIn;
}

That's pretty cool. (even though I expected a robot that played against you... ;D)

But isn't connectfour 7*7? Or did you just run out of room on the protoboard?

And one suggestion: you said you might go with rgb leds; why not an 8*8 rgb led matrix? Or even just a rg led matrix? Driving it would be basically the same, just connecting the extra pins of the 595s and 2803. Then you would also have an extra row and column for whatever you like! (wins/losses, marking whose turn it is, etc)

Thanks for the reply!

I had to double check just to be sure, but, connect four is on a 7x6 grid. I already have a status led to show the current player (the project will be mounted in clear plexi with the player status led illuminating the case. I do plan on an RGB matrix, if for nothing else it will help illuminate each cell more evenly.

Has anyone seen a problem with the current schematic? I still need to add a couple things to it that are already on the prototype (status led, start button)..

One suggestion for the schematic:

You appear to be using pullup/down resistors on the buttons; the arduino has pullups built in. This would just simplify it a bit, you would just need to wire the button from the pin to ground. To use the pullups, just digitalwrite the pin (already set as an input) HIGH, and they are enabled.

Oh, and you're right about the game dimensions. Sorry! :-[

Sorry, I forgot to mention that the 7 column buttons are hooked (yes, via pull-up res) to a CD4021BE input shift register, I don't think the internal pull ups would do me any good in this case...

Oh, nevermind then. I guess I missed that. :-[I'm an idiot... :)

UPDATE!

I've made a few circuit boards for the project (a button board for the top of the case, and a couple control boards (one including an atmega, one without). I'll add the pics below:

Button Board:

Orignal Control Board... It connected to the button board and the matrix via ribbon cable, and the arduino with a mess of jumper wires:

Just after installing jumpers... I hate them, and tried to hide as many as I could under the IC's, but as my local shop doesn't carry any double sided PCB, I had to make do...

After adding smaller components.

Complete! Well, almost... I don't have any spare atmegas or 28 pin dip sockets (they should arrive next week).

And yes, before the complaints... I fully intend on cleaning the flux off before I call it 'done' ;)

Once it is tested, I will be sure to get a video online showing it in action. Only one more vital component is required, 42 Bi-Colour (or RGB) leds... YAY! More soldering ::)

Looks very cool. Are you going to make an enclosure (case) for it?

Coincidently, I just got finished with my first draft of a case for the control board... Nothing but a box with the screw locations and the location of the start button at the moment... but it's almost 5am ;)

(using sketchup for the design of the case, and will post pics tomorrow)

Well cool :D

Are you using frosted plexi on the grid in front of the LED's?

I will be, yes... Right now it's 'hand frosted' using sand paper (really crappy job too), but the final prototype will have proper frosted plexi.

Will you be relasing an ecthable file for the boards? I would love to do this.

yeppers! I intend to release the project both as fully open source (free as in speech and beer), and in kit-form (pre-made, and complete diy).

And there was much rejoycing.

VIDEO Update!

I took a quick video with my brand spankin new iPhone 4 (sick screen btw, absolutely stunning) showing off the second version of my control board prototype. I am still waiting for my Atmega’s to arrive in the mail, so the ‘all in one’ version 3 will have to wait for the time being…

http://www.youtube.com/watch?v=SCfEGnJKMpo ← Watch on youtube for an example of iPhone4 HD video + iMovie on the iPhone.

The difference between when you had the lights on and off was like night and day! Pun intended.. That looks so nice with the lights off. Like they say "beauty is only ever a light switch away". Good job!

I got my Atmega's today from Sparkfun and I was itching all day long waiting to get home and install it...

Well, I did... and....

IT WORKS! ;D [smiley=2vrolijk_08.gif] ;D

I'll be posting a video later tonight. I can finally work on another project without having to completely rip apart (and then re-wire) this one...

Next step is to wrap it in a quick'n'dirty box, laser cutter here I come!

New Video Update!

I made a video showing off the various components in the new prototype, please take a look :)

http://www.youtube.com/watch?v=W_bnW0zSNqc