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
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
// RockPaperScissors.ino : play ro-sham-bo using buttons.
// No copyright, 2020, Garth Zeglin.  This file is
// explicitly placed in the public domain.

// This example implements a rock-paper-scissors game
// using two hobby servos to indicate the computer and
// user choices, three pushbuttons for the user to
// indicate a choice, and a speaker for tone outputs
// for game feedback.
//
// The player sees a countdown movement sequence, then
// has a short window of time to press a button after
// the computer starts moving or the match is invalid.
// If the player moves first, the computer always wins.
// The game automatically cycles back to resting.

#include <Servo.h>

// The input switches are wired as active-low
// pushbuttons.  The 'analog' input pins are used here
// in the digital input mode.
const int ROCK_SWITCH_PIN     = A0;
const int SCISSORS_SWITCH_PIN = A1;
const int PAPER_SWITCH_PIN    = A2;

// The speaker outout.
const int SPEAKER_PIN = 5;

// Hobby servos for the player and computer move
// indicator outputs.
const int P_SERVO_PIN = 8;
const int C_SERVO_PIN = 9;

// servo hardware
Servo player_svo;
Servo computer_svo;

// ================================================
// const values to define game states (could also have
// used enum)
const int WAITING  = 0;
const int ROCK     = 1;
const int SCISSORS = 2;
const int PAPER    = 3;

// calibration tables to maps a game state to a servo angle
const int computer_angles[] = {  0, 63, 93, 123 };
const int player_angles[]   = {  0, 123, 93, 63 };
const int countdown_angle = 30;

// winning move table to return the winning state for a given state
const int win_table[] = {WAITING, PAPER, ROCK, SCISSORS};
  
// state machine indices, defined using enum
enum { IDLE, COUNTDOWN, MOVING, COMPUTER_WIN, PLAYER_WIN,
  DRAW, FAULT, RESET };

// time constants in milliseconds
const long countdown_wait   = 500;
const long valid_input_wait = 400;
const long idle_wait        = 2000;
const long resolution_wait  = 1500;
const long reset_wait       = 1500;
const long melody_wait      = 250;

// state variables for the game
int game_state = IDLE;
int game_counter = 0;
int computer_state = WAITING;
int player_state = WAITING;
long game_timer = 1000;

// state variables for the audio player
int melody_note = 60;
int melody_interval = 7;
int melody_count = 0;
long melody_timer = 0;

// ================================================
void setup()
{
  player_svo.attach(P_SERVO_PIN);
  computer_svo.attach(C_SERVO_PIN);
  
  // issue an initial servo command to the reset condition
  player_move(WAITING);
  computer_move(WAITING);

  Serial.begin(115200);
  Serial.println("Welcome to rock, scissors, paper.");
}

// ================================================
void loop()
{
  // The timestamp in milliseconds for the last polling
  // cycle, used to compute the exact interval between
  // output updates.
  static unsigned long last_update_clock = 0;

  // Read the millisecond clock.
  unsigned long now = millis();

  // Compute the time elapsed since the last poll.
  // This will correctly handle wrapround of the 32-bit
  // long time value given the properties of
  // twos-complement arithmetic.
  unsigned long interval = now - last_update_clock;
  last_update_clock = now;

  // Always advance the game timer; when it becomes
  // negative the current phase has expired.
  game_timer = game_timer - interval;

  // Always keep advancing the pseudorandom generator.
  long next_random = random(1,4);

  // Advance the melody player if needed.
  if (melody_count >= 0) {
    melody_timer = melody_timer - interval;
    if (melody_timer < 0) {
      melody_timer = melody_wait;
      play_next_note();
    }
  }
  
  // Always read the player switches.  This provides a
  // single location to perform input validation.  The
  // hardware is wired for active-low logic.
  bool rock_pressed     = !digitalRead(ROCK_SWITCH_PIN);
  bool scissors_pressed = !digitalRead(SCISSORS_SWITCH_PIN);
  bool paper_pressed    = !digitalRead(PAPER_SWITCH_PIN);

  // Reduce the switch selection input to a single
  // value, rejecting multiple pushes.
  int user_input_state = WAITING; // default neutral value
  if ( rock_pressed && !scissors_pressed && !paper_pressed) user_input_state = ROCK;
  if (!rock_pressed &&  scissors_pressed && !paper_pressed) user_input_state = SCISSORS;
  if (!rock_pressed && !scissors_pressed &&  paper_pressed) user_input_state = PAPER;
  
  // Run one update cycle of the game state machine.
  switch(game_state) {

  case IDLE: // no one is moving
    if (user_input_state != WAITING) {
      // player has played early, let's win!
      player_state = user_input_state;
      computer_state = win_table[player_state];
      computer_move(computer_state);
      player_move(player_state);
      game_state = COMPUTER_WIN;
      game_timer = resolution_wait;
      Serial.println("Player played early, computer wins.");
      start_arpeggio(60, 7, 3);
      
    } else if (game_timer < 0) {
      // time to start the countdown
      Serial.println("Starting countdown.");      
      game_timer = countdown_wait;
      game_counter = 3;
      countdown_beat(true);
      game_state = COUNTDOWN;
    }
    break;
    
  case COUNTDOWN:
    // both are moving 1, 2, .. in preparation
    if (user_input_state != WAITING) {
      // player has played early, let's win!
      player_state = user_input_state;
      computer_state = win_table[player_state];
      computer_move(computer_state);
      player_move(player_state);
      game_state = COMPUTER_WIN;
      game_timer = resolution_wait;
      Serial.println("Player played early, computer wins.");
      start_arpeggio(60, 7, 3);
      
    } else if (game_timer < 0) {
      // time to continue the countdown animation
      game_counter = game_counter - 1;
      if (game_counter < 0) { // time to choose a move      
	game_timer = valid_input_wait;
	computer_state = next_random;
	computer_move(computer_state);
	game_state = MOVING;
	Serial.println("Computer moved.");
      } else {
	// continue the countdown animation
	countdown_beat((game_counter % 2) == 1);
	game_timer = countdown_wait;
      }
    }
    break;
    
  case MOVING:
    // computer is moving, wait for user input within a
    // short interval
    if (user_input_state != WAITING) {
      player_state = user_input_state;
      player_move(player_state);
      game_timer = resolution_wait;
      Serial.println("Player responded.");
      // decide the winner
      if (computer_state == player_state) {
	game_state = DRAW;
	Serial.println("Draw, no winner.");
	start_arpeggio(66, -6, 2);	
      }	 else if (computer_state == win_table[player_state]) {
	game_state = COMPUTER_WIN;
	Serial.println("Computer wins.");
	start_arpeggio(60, 7, 3);
	
      } else {
	game_state = PLAYER_WIN;
	Serial.println("Player wins.");
	start_arpeggio(55, 12, 3);	
      }

    } else if (game_timer < 0) {
      // if the user did not respond in time
      game_timer = resolution_wait;
      game_state = FAULT;
      Serial.println("Player did not respond, game fault.");
      start_arpeggio(60, -12, 3);	      
    }
    break;

    // In every game outcome, wait for servos to finish
    // moving, then reset.
  case COMPUTER_WIN:
  case PLAYER_WIN:
  case DRAW:
  case FAULT:
    if (game_timer < 0) {
      game_timer = reset_wait;
      computer_state = WAITING;
      player_state = WAITING;
      computer_move(computer_state);
      player_move(player_state);
      game_state = RESET;
    }
    break;

  case RESET: // returning to start
    if (game_timer < 0) {
      game_timer = idle_wait;
      game_state = IDLE;
    }
    break;
  }

  // add a short delay to not overwhelm the Tinkercad simulator
  delay(20);
}
// ================================================
// movement primitives
void player_move(int state)
{
  player_svo.write(player_angles[state]);
  Serial.print("Player move: ");
  Serial.println(state);
}

void computer_move(int state)
{
  computer_svo.write(computer_angles[state]);
  Serial.print("Computer move: ");
  Serial.println(state);
  
}
void countdown_beat(bool forward)
{
  if (forward) {
    player_svo.write(countdown_angle);
    computer_svo.write(countdown_angle);
    Serial.print("beat forward...");
  } else {
    player_svo.write(0);
    computer_svo.write(0);
    Serial.println("back...");
  }
}

// ================================================
// sound primitives
void start_arpeggio(int start, int interval, int length)
{
  melody_note = start;
  melody_interval = interval;
  melody_count = length;
  melody_timer = melody_wait;  
  play_next_note();
}

// choose and play the next note in the melody sequence
void play_next_note(void)
{
  if (melody_count > 0) {
    float freq = midi_to_freq(melody_note);
    tone(SPEAKER_PIN, freq);

    // advance the arpeggio
    melody_note = melody_note + melody_interval;
    melody_count = melody_count - 1;

  } else if (melody_count == 0) {
    // when melody_count is zero, silence the speaker
    // and set it to -1 to represent the idle state
    melody_count = -1;
    noTone(SPEAKER_PIN);
  }
}
  
float midi_to_freq(int midi_note)
{
  const int   MIDI_A0 = 21;
  const float freq_A0 = 27.5;
  return freq_A0 * pow(2.0, ((float)(midi_note - MIDI_A0)) / 12.0);
}
// ================================================