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:
non-blocking event polling loop to simultanously process input and output
switch-case state machine structure to manage game control flow
timer variables to schedule future events
symbolic numeric constants
Tinkercad Circuit¶
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);
}
// ================================================
|