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// 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// ================================================