Arduino ‘Capture the Flag’ Game Controller

Mid summer 2017, some of the guys at my weekend airsoft group asked me to help them out with a little project. They wanted to build a control system for game play that had lights and a siren to help let people know when a game starts or ends and more. I figured it would be a quick little project, and I was looking for something to build with a couple Arduino Uno’s that I had lying around, so I took them up on it. This is the first project I have done on an Arduino, however I previously use the ATMEL AVR chip to do this kind of thing so I am very familiar with this stuff.

The basic functionality they asked for was a game timer and lights to represent 2 teams for capture the flag (my favourite game). When we play outdoors one of the big problems we have is letting both teams know exactly when the game starts (very big field). We have tried whistles (usually lose them), yelling (usually resulting in sore throats and a headache), and Radios (usually forget them , dead batteries, or wrong channel). This is really annoying, so I figured I would add functionality for a 30 second countdown for a game start siren.

The first iteration of this system needed 5 buttons.

  • Start Game
  • Stop Game / Reset
  • Choose Game Duration (10,20,30,50, unlimited minutes)
  • Team 1 (Red)
  • Team 2 (Yellow)

On the second iteration, I reduce it to 3 buttons. Less soldering and connections is a good thing!

  • Choose Game Duration (10,20,30,50, unlimited minutes), or stop game
  • Team 1 (Red) , or start game
  • Team 2 (Yellow) , or start game

I needed 5 bright LED’s to show the current game duration, I used some LEDs from Amazon designed to run on 12V. Also two very bright (high current) 12V trailer LED’s to show Team 1, Team 2. Finally I needed a 12V Siren, which I found in my junk pile, an old car alarm siren from 1990! ha , I knew I would use it eventually, I am not a pack rat or anything like that 😉

I programmed it originally on an Arduino Uno, but when I was done I decided that I would deploy it on an Ardinuo Nano ($5, vs $15 for the Uno) so I could keep the Uno for other development projects because it has nice headers for jumpers but the Nano needs to be soldered.

I really did not want to jump through a lot of hoops to get a development environment working on my PC, so I used https://create.arduino.cc for editing and uploading the binary to my Arduino over USB. It worked really well, but not as good as a real development environment. It can be frustrating having such a limited code editor without context sensitive help. But good enough, so on we go….

I began by figuring out what pins I should use for each function and looking up how to initialise them.

The Pins must be initialized BEFORE you use them or the magic smoke comes out, this is usually done during the initialisation of the program before the main loop begins. Take a look at the setup() function in the code link.

I tried to break the program up as much as possible and comment a bunch as well. It may be useful for someone looking to learn the very basics of Arduino. I eventually rewrote all the code to make it much cleaner and easier to modify.

Here is a poorly lit Prototype Pic!

Early stages of connecting the Relay board to the Arduino Uno.

I gave the first (completely soldered) version to the guys to put into an enclosure, and forgot to take a picture, so here is a pic of the second version using a solid state relay.

Version 2

The solid stated relay proved to be total overkill, so I have replaced it with some power N-Type MOSFETS (IRLB3034PBF HEXFET Power MOSFET TO-220) from Amazon, very simple to hook up.

Pin Order: Gate-Drain-Source

Here are some pics of the finished version 3 of the project:

MOSFETS for Trailer lights and Siren

Arduino Nano mounted to PCB Board

Siren

Inside of Toolbox with hole for Siren

Final Result (Still needs some clear covers over the trailer lights)

The entire project source code can be found here Arduino Airsoft Game System Code V3. It is free for anyone to use, any way they want! Have fun!

I have included the full source below in case the link does not work:

/*
Ron Ostafichuk, Airsoft Game System Code V3 using MOSFETS
Change Log 
Sep 6 22:21 flash winner light for 5 minutes after game ends
Sep 7 Chirp Siren 3 times at 1 minute from game end, 2 times on game start countdown
Sep 8 Change Time Selection to 1 button that cycles through the different time options 10-20-30-50-unlimited
Sep 20 Added Game State To try to manage functionality better
Nov 4 Merge Stop button functionality into Game time selector to get rid of 1 button. 
  Got rid of Middle Light , flash selected gametime light instead to show system is working.
  Got rid of Start button , if game is waiting, hitting Team1 OR Team2 will start the game!
Nov 5 code and comments cleanup, fixed game length lights test on reset
Nov 10 Final changes to work with new Hardware
*/

// The Game state controls the overall flow of the logic for a game
enum GAMESTATE
{
  GS_INIT=0, // system just booted up
  GS_WAITING, // waiting for game start, only the game time button is functional, Team2 or Team2 buttons will start the game
  GS_STARTCOUNTDOWN, // then waiting 30 seconds to start game, chirp every 2 seconds during this state
  GS_GAMEINPROGRESS, // game in progress (disable game time btn)
  GS_GAMEONEMINUTEWARN, // 1 chirp to indicate one minute left
  GS_GAMEEND, // Game over, Siren for 10 seconds disable all btns except Game Length for reset , flash winning team for 3 minutes
};

GAMESTATE nGameState = GS_INIT;


const int BTN_GAME_LENGTH = 3; // will also stop a game in progress
const int BTN_TEAM1 = 5;
const int BTN_TEAM2 = 4;

const int LIGHT_TEAM1 = 9;
const int LIGHT_TEAM2 = 10;
const int SIREN = 11;

// relay acts different than lights
const int RELAY_ON = HIGH;
const int RELAY_OFF = LOW;

// Note: built in LED is also pin 13

unsigned long nTime_ms = 0;
unsigned long nFlashCounter = 0; // use this to control the rate of flashing of the lights
unsigned long nLastTime_ms = 0;

unsigned long nGameStart_ms = 0; // time the start button was pressed (will flash for 30 seconds then sound siren for 4)
unsigned long nSirenStop_ms = 0;
unsigned long nTeamNumberSelected = 0;
unsigned long nGameLength_ms = 8*60*60*1000; // default 8 hrs
unsigned long nGameLength_minutes = 8*60; // default 8 hrs

// Button Debounce logic
unsigned long nBtnGameLengthDown_ms = 0; // start time for GameLength button down (must exceed 1/8 second to trigger?)

// the setup function runs once when you press reset or power the board
void setup() {
  // declare the button pins as an input_PULLUP
  pinMode(BTN_TEAM1, INPUT_PULLUP);
  pinMode(BTN_TEAM2, INPUT_PULLUP);
  pinMode(BTN_GAME_LENGTH, INPUT_PULLUP);
  
  pinMode(LIGHT_TEAM1, OUTPUT);
  pinMode(LIGHT_TEAM2, OUTPUT);
  pinMode(SIREN, OUTPUT);

  digitalWrite(LIGHT_TEAM1, RELAY_OFF);
  digitalWrite(LIGHT_TEAM2, RELAY_OFF);
  digitalWrite(SIREN, RELAY_OFF);

  // Note only the UNO can use these pins as Digital OUTPUTS
  pinMode(A0, OUTPUT); // led to show 10 minute game
  pinMode(A1, OUTPUT); // led to show 20 minute game
  pinMode(A2, OUTPUT); // led to show 30 minute game
  pinMode(A3, OUTPUT); // led to show 50 minute game
  pinMode(A4, OUTPUT); // led to show unlimited game
  
  digitalWrite(A0, LOW);  
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A3, LOW);
  digitalWrite(A4, LOW);
  
  delay(100);
  
  Reset();
}

void ChirpSiren()
{
  digitalWrite(SIREN, RELAY_ON); // start the SIREN!
  nSirenStop_ms = nTime_ms + 100; // will stop in 100 ms
}

void ControlLightForGameLengthMinutes(int nState)
{
  // nState is either HIGH (for on) or LOW, this is done so I can flash the game length lights during the start countdown or game end
  // turn all off first
  digitalWrite(A0, LOW);  
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A3, LOW);
  digitalWrite(A4, LOW);
  
  if( nGameLength_minutes >= 10)
    digitalWrite(A0, nState);
  
  if( nGameLength_minutes >= 20 )
    digitalWrite(A1, nState);
    
  if( nGameLength_minutes >= 30 )
    digitalWrite(A2, nState);
    
  if( nGameLength_minutes >= 50 )
    digitalWrite(A3, nState);
    
  if( nGameLength_minutes >= 8*60 )
    digitalWrite(A4, nState);
}

void SetGameLengthMinutes(unsigned long nMinutes)
{
    nGameLength_minutes = nMinutes;
    nGameLength_ms = nMinutes*60*1000;
    // set leds to show GAME length
    ControlLightForGameLengthMinutes(HIGH);
}

void Reset()
{
  nGameStart_ms = 0;
  nSirenStop_ms = 0;
  nTeamNumberSelected = 0; // no team selected by default
  nBtnGameLengthDown_ms = 0;

  // LEDS OFF
  digitalWrite(A0, LOW);
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A3, LOW);
  digitalWrite(A4, LOW);

  // relays off  
  digitalWrite(SIREN, RELAY_OFF);
  digitalWrite(LIGHT_TEAM1, RELAY_OFF);
  digitalWrite(LIGHT_TEAM2, RELAY_OFF);
  
  delay(250);
  unsigned long nMinutes = nGameLength_minutes; // preserve last game length
  
  // test function and lights
  digitalWrite(LIGHT_TEAM1, RELAY_ON);
  digitalWrite(LIGHT_TEAM2, RELAY_ON);
  
  SetGameLengthMinutes(10);
  delay(250);
  SetGameLengthMinutes(20);
  delay(250);
  SetGameLengthMinutes(30);
  delay(250);
  
  SetGameLengthMinutes(50);
  delay(250);
  
  SetGameLengthMinutes(8*60);
  digitalWrite(SIREN, RELAY_ON); // on for short chirp to prove siren is working
  delay(250);

  // RELAYS off
  digitalWrite(LIGHT_TEAM1, RELAY_OFF);
  digitalWrite(LIGHT_TEAM2, RELAY_OFF);
  digitalWrite(SIREN, RELAY_OFF);

  // LEDS OFF
  digitalWrite(A0, LOW);
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A3, LOW);
  digitalWrite(A4, LOW);

  // Restore Game Length here
  SetGameLengthMinutes(nMinutes);
  
  nGameState = GS_WAITING;
}


// the loop function runs over and over again forever
void loop() {
  nTime_ms = millis(); // get time at start of Loop
  nFlashCounter = nTime_ms/250; // this is used for flashing the lights, with an integer that represents 1/4 of a second we will get a flash cycle of 1 per half second

  // handle Button debounce for Game Length button only (not needed for other buttons)
  bool bGameLengthChange = false; // this flag indicates if the button was held down for over 100 ms and then released
  int nBtnGameLength = digitalRead(BTN_GAME_LENGTH);  
  if( nBtnGameLengthDown_ms == 0 )
  {
    // check for a down signal
    if( nBtnGameLength == LOW ){
      nBtnGameLengthDown_ms = nTime_ms; // record time of btn down
    }
  } else
  {
    // btn down time exists, so check for button release
    if(nBtnGameLength == HIGH )
    {
      // btn released
      // check for 60 ms down on release
      if( nBtnGameLengthDown_ms > 0 && nBtnGameLengthDown_ms + 60 < nTime_ms )
      {
        bGameLengthChange = true; // button was held down for over 60 ms and then released
        nBtnGameLengthDown_ms = 0; // reset
      }
      
      // always reset time on button up (effectively ignore false button presses)
      nBtnGameLengthDown_ms = 0;
    }
  }

  // Handle GAME STATE : WAITING
  // Functions allowed in the state:
  // 1. change the game length
  // 2. start game with current game length setting
  // ===============================================
  if( nGameState == GS_WAITING )
  {

    // only change game length if we are waiting for a game to begin
    if( bGameLengthChange )
    {
      // only allow game length change if game is not inprogress
      unsigned long nNewLengthMinutes = 10;
      if( nGameLength_minutes <= 10)
        nNewLengthMinutes = 20;
      else if( nGameLength_minutes > 10 && nGameLength_minutes <= 20)
        nNewLengthMinutes = 30;
      else if( nGameLength_minutes > 20 && nGameLength_minutes <= 30)
        nNewLengthMinutes = 50;
      else if( nGameLength_minutes > 30 && nGameLength_minutes <= 50)
        nNewLengthMinutes = 8*60;
      else
        nNewLengthMinutes = 10; // return to beginning
              
      SetGameLengthMinutes(nNewLengthMinutes); // set the LEDS accordingly
    }

    if( digitalRead(BTN_TEAM1)  == LOW || digitalRead(BTN_TEAM2)  == LOW  )
    {
      // GAME STARTED BY USER!!!!!!!!!
      // =====================================
      nTeamNumberSelected = 0; // no team selected by default
      nBtnGameLengthDown_ms = 0;
      nGameState = GS_STARTCOUNTDOWN;
      nGameStart_ms = nTime_ms+1;
      // chirp siren on game start btn
      ChirpSiren();
    } 
  }
  else if( nGameState == GS_STARTCOUNTDOWN)
  {
    // Handle GAME STATE : Start Countdown
    // Functions allowed in the state:
    // 1. flashing lights until the game starts
    // 2. After 30 seconds of countdown, switch state to GameInProgress
    // 3. Check for possible reset request by user hitting the game length button
    // ==========================================================================
    
    // handle flashing lights
    if( nFlashCounter % 2 == 0 )
    {
      // turn lights on
      ControlLightForGameLengthMinutes(HIGH);
      digitalWrite(LIGHT_TEAM1, RELAY_ON);
      digitalWrite(LIGHT_TEAM2, RELAY_ON);
    } 
    else
    {
      // turn off
      ControlLightForGameLengthMinutes(LOW);
      digitalWrite(LIGHT_TEAM1, RELAY_OFF);
      digitalWrite(LIGHT_TEAM2, RELAY_OFF);
    }

    // Check for reset 
    if( bGameLengthChange )
      Reset();
    
    // check for 30 second countdown complete
    if( nGameStart_ms > 0 && nGameStart_ms + 30000 < nTime_ms && nTime_ms < nGameStart_ms + 37000 && nSirenStop_ms == 0 )
    {
      nGameState = GS_GAMEINPROGRESS;
      ControlLightForGameLengthMinutes(HIGH); // restore the game time lights
      nTeamNumberSelected = 0; // make sure no team is selected at game start!
      
      digitalWrite(LIGHT_TEAM1, RELAY_OFF); // turn off team lights in case they are on
      digitalWrite(LIGHT_TEAM2, RELAY_OFF);
      
      // start the SIREN for 7 seconds, to signal start of game
      nSirenStop_ms = nGameStart_ms + 37000;
      if( digitalRead(SIREN) != RELAY_ON )
        digitalWrite(SIREN, RELAY_ON);
    }
  }
  else if( nGameState == GS_GAMEINPROGRESS || nGameState == GS_GAMEONEMINUTEWARN )
  {  
    // Handle GAME STATE : GAME IN PROGRESS, or 1 minute to end of game
    // Functions allowed in the state:
    // 1. Flash the Game duration lights
    // 2. Allow Team1 or Team2 to be selected
    // 3. Allow reset of game
    // 4. At 1 minute before game end do a single Siren chirp
    // 4. Check for game time end to end the game!
    // ============================================
    
    // flash Game Duration Light when the game is in progress!
    if( nFlashCounter % 2 == 0 )
    {
      ControlLightForGameLengthMinutes(HIGH); // turn on the selected game time lights
    } 
    else
    {
      ControlLightForGameLengthMinutes(LOW); // all off
    }
  
    if( bGameLengthChange )
    {
      Reset(); // reset the game!!!
    }
  
    // check for Team Capture, but do not allow flag change AFTER game ends!
    if( digitalRead(BTN_TEAM1) == LOW && nTeamNumberSelected != 1)
    {
      nTeamNumberSelected = 1; // lights will be set below
    }
    if( digitalRead(BTN_TEAM2) == LOW && nTeamNumberSelected != 2 )
    {
      nTeamNumberSelected = 2;
    }
    
    if( nTime_ms >= nGameStart_ms + nGameLength_ms - (unsigned long)60000 && nGameState < GS_GAMEONEMINUTEWARN )
    {
      // one minute to game end, chirp the siren 1 time and switch states so we only chirp once
      nGameState = GS_GAMEONEMINUTEWARN;
      ChirpSiren();
    }
    
    // check for Game End
    if( nTime_ms >= nGameStart_ms + nGameLength_ms && nTime_ms < nGameStart_ms + nGameLength_ms + (unsigned long)1100 )
    {
      // STOP THE GAME
      nGameState = GS_GAMEEND;
      // start a 12 second siren
      nSirenStop_ms = nTime_ms + (unsigned long)12000;
      if( digitalRead(SIREN) != RELAY_ON )
        digitalWrite(SIREN, RELAY_ON); // start the SIREN!
    }

  } else if( nGameState == GS_GAMEEND )
  {
    // flash the winning team light for 3 minutes!
    if( nGameStart_ms > 0 && nTime_ms > nGameStart_ms + nGameLength_ms + (unsigned long)3*60*1000 
        || bGameLengthChange )
    {
        Reset(); // automatically reset after 5 minutes past end of game, or if user hits the game length button
    }
  }
  
  
  
  // check for end of Siren events (this is game state Independent)
  // ==============================================================
  if( nSirenStop_ms > 0 && nSirenStop_ms < nTime_ms )
  {
    nSirenStop_ms = 0;
    digitalWrite(SIREN, RELAY_OFF); // stop the SIREN!
  }

  
  // Show the Team Capture Status when Game in progress or over (FLASH LIGHTS)
  if( nGameState >= GS_GAMEINPROGRESS )
  {
    // flash the Team lights always to prevent overheat and reduce power used by 50% ( The Trailer LED lights I am using draw a lot of current, want battery to last over 6 hours)
    if( nTeamNumberSelected == 1 )
    {
      if( nFlashCounter % 2 == 0 )
      {
        if( digitalRead(LIGHT_TEAM1) != RELAY_ON )
          digitalWrite(LIGHT_TEAM1, RELAY_ON);   // turn the LED on
      } else
      {
        if( digitalRead(LIGHT_TEAM1) != RELAY_OFF )
          digitalWrite(LIGHT_TEAM1, RELAY_OFF);    // turn the LED off
      }
      digitalWrite(LIGHT_TEAM2, RELAY_OFF);
    } else if( nTeamNumberSelected == 2 )
    {
      if( nFlashCounter % 2 == 0 )
      {
        if( digitalRead(LIGHT_TEAM2) != RELAY_ON )
          digitalWrite(LIGHT_TEAM2, RELAY_ON);   // turn the LED on
      } else
      {
        if( digitalRead(LIGHT_TEAM2) != RELAY_OFF )
          digitalWrite(LIGHT_TEAM2, RELAY_OFF);    // turn the LED off
      }
      digitalWrite(LIGHT_TEAM1, RELAY_OFF);
    } else
    {
      // turn off T1 and T2
      if( digitalRead(LIGHT_TEAM1) != RELAY_OFF)
        digitalWrite(LIGHT_TEAM1, RELAY_OFF);
      if( digitalRead(LIGHT_TEAM2) != RELAY_OFF)
        digitalWrite(LIGHT_TEAM2, RELAY_OFF);
    }
  }
  
  nLastTime_ms = nTime_ms; // use this to figure out how long the main loop is taking (for debugging)
}

Conclusion:
I have found the arduino to be a fantastic platform for moderate complexity projects. I had so much fun building this that I want to build another project A.S.A.P.