RockPaperScissors Arduino Sketch

This sketch implements a rock-paper-scissors game using three pushbuttons for user input, two hobby servos in lieu of human hand for displaying the countdown and selections, and tone feedback using a speaker.

The code is intended as a demonstration for several techniques:

  1. non-blocking event polling loop to simultanously process input and output

  2. switch-case state machine structure to manage game control flow

  3. timer variables to schedule future events

  4. symbolic numeric constants

Tinkercad Circuit

../_images/RockPaperScissors-circuit.png

Reference circuit for RockPaperScissors in Tinkercad. The annotation labels indicate the button functions and specific locations of the hobby servos.

Full Source Code

The full code is all in one file RockPaperScissors.ino.

  1// RockPaperScissors.ino : play ro-sham-bo using buttons.
  2// No copyright, 2020, Garth Zeglin.  This file is
  3// explicitly placed in the public domain.
  4
  5// This example implements a rock-paper-scissors game
  6// using two hobby servos to indicate the computer and
  7// user choices, three pushbuttons for the user to
  8// indicate a choice, and a speaker for tone outputs
  9// for game feedback.
 10//
 11// The player sees a countdown movement sequence, then
 12// has a short window of time to press a button after
 13// the computer starts moving or the match is invalid.
 14// If the player moves first, the computer always wins.
 15// The game automatically cycles back to resting.
 16
 17#include <Servo.h>
 18
 19// The input switches are wired as active-low
 20// pushbuttons.  The 'analog' input pins are used here
 21// in the digital input mode.
 22const int ROCK_SWITCH_PIN     = A0;
 23const int SCISSORS_SWITCH_PIN = A1;
 24const int PAPER_SWITCH_PIN    = A2;
 25
 26// The speaker outout.
 27const int SPEAKER_PIN = 5;
 28
 29// Hobby servos for the player and computer move
 30// indicator outputs.
 31const int P_SERVO_PIN = 8;
 32const int C_SERVO_PIN = 9;
 33
 34// servo hardware
 35Servo player_svo;
 36Servo computer_svo;
 37
 38// ================================================
 39// const values to define game states (could also have
 40// used enum)
 41const int WAITING  = 0;
 42const int ROCK     = 1;
 43const int SCISSORS = 2;
 44const int PAPER    = 3;
 45
 46// calibration tables to maps a game state to a servo angle
 47const int computer_angles[] = {  0, 63, 93, 123 };
 48const int player_angles[]   = {  0, 123, 93, 63 };
 49const int countdown_angle = 30;
 50
 51// winning move table to return the winning state for a given state
 52const int win_table[] = {WAITING, PAPER, ROCK, SCISSORS};
 53  
 54// state machine indices, defined using enum
 55enum { IDLE, COUNTDOWN, MOVING, COMPUTER_WIN, PLAYER_WIN,
 56  DRAW, FAULT, RESET };
 57
 58// time constants in milliseconds
 59const long countdown_wait   = 500;
 60const long valid_input_wait = 400;
 61const long idle_wait        = 2000;
 62const long resolution_wait  = 1500;
 63const long reset_wait       = 1500;
 64const long melody_wait      = 250;
 65
 66// state variables for the game
 67int game_state = IDLE;
 68int game_counter = 0;
 69int computer_state = WAITING;
 70int player_state = WAITING;
 71long game_timer = 1000;
 72
 73// state variables for the audio player
 74int melody_note = 60;
 75int melody_interval = 7;
 76int melody_count = 0;
 77long melody_timer = 0;
 78
 79// ================================================
 80void setup()
 81{
 82  player_svo.attach(P_SERVO_PIN);
 83  computer_svo.attach(C_SERVO_PIN);
 84  
 85  // issue an initial servo command to the reset condition
 86  player_move(WAITING);
 87  computer_move(WAITING);
 88
 89  Serial.begin(115200);
 90  Serial.println("Welcome to rock, scissors, paper.");
 91}
 92
 93// ================================================
 94void loop()
 95{
 96  // The timestamp in milliseconds for the last polling
 97  // cycle, used to compute the exact interval between
 98  // output updates.
 99  static unsigned long last_update_clock = 0;
100
101  // Read the millisecond clock.
102  unsigned long now = millis();
103
104  // Compute the time elapsed since the last poll.
105  // This will correctly handle wrapround of the 32-bit
106  // long time value given the properties of
107  // twos-complement arithmetic.
108  unsigned long interval = now - last_update_clock;
109  last_update_clock = now;
110
111  // Always advance the game timer; when it becomes
112  // negative the current phase has expired.
113  game_timer = game_timer - interval;
114
115  // Always keep advancing the pseudorandom generator.
116  long next_random = random(1,4);
117
118  // Advance the melody player if needed.
119  if (melody_count >= 0) {
120    melody_timer = melody_timer - interval;
121    if (melody_timer < 0) {
122      melody_timer = melody_wait;
123      play_next_note();
124    }
125  }
126  
127  // Always read the player switches.  This provides a
128  // single location to perform input validation.  The
129  // hardware is wired for active-low logic.
130  bool rock_pressed     = !digitalRead(ROCK_SWITCH_PIN);
131  bool scissors_pressed = !digitalRead(SCISSORS_SWITCH_PIN);
132  bool paper_pressed    = !digitalRead(PAPER_SWITCH_PIN);
133
134  // Reduce the switch selection input to a single
135  // value, rejecting multiple pushes.
136  int user_input_state = WAITING; // default neutral value
137  if ( rock_pressed && !scissors_pressed && !paper_pressed) user_input_state = ROCK;
138  if (!rock_pressed &&  scissors_pressed && !paper_pressed) user_input_state = SCISSORS;
139  if (!rock_pressed && !scissors_pressed &&  paper_pressed) user_input_state = PAPER;
140  
141  // Run one update cycle of the game state machine.
142  switch(game_state) {
143
144  case IDLE: // no one is moving
145    if (user_input_state != WAITING) {
146      // player has played early, let's win!
147      player_state = user_input_state;
148      computer_state = win_table[player_state];
149      computer_move(computer_state);
150      player_move(player_state);
151      game_state = COMPUTER_WIN;
152      game_timer = resolution_wait;
153      Serial.println("Player played early, computer wins.");
154      start_arpeggio(60, 7, 3);
155      
156    } else if (game_timer < 0) {
157      // time to start the countdown
158      Serial.println("Starting countdown.");      
159      game_timer = countdown_wait;
160      game_counter = 3;
161      countdown_beat(true);
162      game_state = COUNTDOWN;
163    }
164    break;
165    
166  case COUNTDOWN:
167    // both are moving 1, 2, .. in preparation
168    if (user_input_state != WAITING) {
169      // player has played early, let's win!
170      player_state = user_input_state;
171      computer_state = win_table[player_state];
172      computer_move(computer_state);
173      player_move(player_state);
174      game_state = COMPUTER_WIN;
175      game_timer = resolution_wait;
176      Serial.println("Player played early, computer wins.");
177      start_arpeggio(60, 7, 3);
178      
179    } else if (game_timer < 0) {
180      // time to continue the countdown animation
181      game_counter = game_counter - 1;
182      if (game_counter < 0) { // time to choose a move      
183	game_timer = valid_input_wait;
184	computer_state = next_random;
185	computer_move(computer_state);
186	game_state = MOVING;
187	Serial.println("Computer moved.");
188      } else {
189	// continue the countdown animation
190	countdown_beat((game_counter % 2) == 1);
191	game_timer = countdown_wait;
192      }
193    }
194    break;
195    
196  case MOVING:
197    // computer is moving, wait for user input within a
198    // short interval
199    if (user_input_state != WAITING) {
200      player_state = user_input_state;
201      player_move(player_state);
202      game_timer = resolution_wait;
203      Serial.println("Player responded.");
204      // decide the winner
205      if (computer_state == player_state) {
206	game_state = DRAW;
207	Serial.println("Draw, no winner.");
208	start_arpeggio(66, -6, 2);	
209      }	 else if (computer_state == win_table[player_state]) {
210	game_state = COMPUTER_WIN;
211	Serial.println("Computer wins.");
212	start_arpeggio(60, 7, 3);
213	
214      } else {
215	game_state = PLAYER_WIN;
216	Serial.println("Player wins.");
217	start_arpeggio(55, 12, 3);	
218      }
219
220    } else if (game_timer < 0) {
221      // if the user did not respond in time
222      game_timer = resolution_wait;
223      game_state = FAULT;
224      Serial.println("Player did not respond, game fault.");
225      start_arpeggio(60, -12, 3);	      
226    }
227    break;
228
229    // In every game outcome, wait for servos to finish
230    // moving, then reset.
231  case COMPUTER_WIN:
232  case PLAYER_WIN:
233  case DRAW:
234  case FAULT:
235    if (game_timer < 0) {
236      game_timer = reset_wait;
237      computer_state = WAITING;
238      player_state = WAITING;
239      computer_move(computer_state);
240      player_move(player_state);
241      game_state = RESET;
242    }
243    break;
244
245  case RESET: // returning to start
246    if (game_timer < 0) {
247      game_timer = idle_wait;
248      game_state = IDLE;
249    }
250    break;
251  }
252
253  // add a short delay to not overwhelm the Tinkercad simulator
254  delay(20);
255}
256// ================================================
257// movement primitives
258void player_move(int state)
259{
260  player_svo.write(player_angles[state]);
261  Serial.print("Player move: ");
262  Serial.println(state);
263}
264
265void computer_move(int state)
266{
267  computer_svo.write(computer_angles[state]);
268  Serial.print("Computer move: ");
269  Serial.println(state);
270  
271}
272void countdown_beat(bool forward)
273{
274  if (forward) {
275    player_svo.write(countdown_angle);
276    computer_svo.write(countdown_angle);
277    Serial.print("beat forward...");
278  } else {
279    player_svo.write(0);
280    computer_svo.write(0);
281    Serial.println("back...");
282  }
283}
284
285// ================================================
286// sound primitives
287void start_arpeggio(int start, int interval, int length)
288{
289  melody_note = start;
290  melody_interval = interval;
291  melody_count = length;
292  melody_timer = melody_wait;  
293  play_next_note();
294}
295
296// choose and play the next note in the melody sequence
297void play_next_note(void)
298{
299  if (melody_count > 0) {
300    float freq = midi_to_freq(melody_note);
301    tone(SPEAKER_PIN, freq);
302
303    // advance the arpeggio
304    melody_note = melody_note + melody_interval;
305    melody_count = melody_count - 1;
306
307  } else if (melody_count == 0) {
308    // when melody_count is zero, silence the speaker
309    // and set it to -1 to represent the idle state
310    melody_count = -1;
311    noTone(SPEAKER_PIN);
312  }
313}
314  
315float midi_to_freq(int midi_note)
316{
317  const int   MIDI_A0 = 21;
318  const float freq_A0 = 27.5;
319  return freq_A0 * pow(2.0, ((float)(midi_note - MIDI_A0)) / 12.0);
320}
321// ================================================