Build an AI goalkeeper that learns where you shoot

Take penalties with the joystick in a browser football game. The goalkeeper is an AI on your UNO: it remembers your shots, predicts where you will shoot next and dives there. People are worse at being random than they think!
  • Level:Advanced
  • Duration: 1 h 15 min
  • Ages 12+
  • Platform: Arduino

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build an AI goalkeeper that learns where you shoot – EEMENTOR STEM™ lesson

People are bad at being random. Ask a friend to shoot penalties left, centre or right without a plan, and after a while patterns appear: they rarely shoot the same side three times, or they always switch after a miss. In this lesson your UNO becomes a goalkeeper AI that finds those patterns. You flick the joystick to shoot in AI goalkeeper on the Arduino Playground. Before every shot, the UNO looks at what you did in the past, estimates the chance of each direction and dives where your next shot is most likely to go. The screen shows its predictions. Can you beat an AI that learns from you?

What you'll learn

  • How to predict the next choice from counts of past choices
  • What a Markov model is: predictions based on the previous step
  • How to blend two models and turn counts into probabilities
  • Why being unpredictable is hard, and how an AI can use that

What you need

  • UNO R3 board and USB cable (UNO R3 Starter Kit)
  • Joystick module (UNO R3 Starter Kit)
  • A computer with Chrome or Edge
  • Breadboard and jumper wires (UNO R3 Starter Kit)

Wiring

Breadboard wiring diagram: Build an AI goalkeeper that learns where you shoot
Breadboard view. Check each connection against the wiring table below.
From To
Joystick GND GND
Joystick +5V 5V
Joystick VRx UNO A0
Joystick VRy UNO A1
Example screen for: Build an AI goalkeeper that learns where you shoot
Example screen with sample values. Yours may look different.

The sketch

No library is needed. The memory is kept while the UNO runs; when you connect the game the UNO restarts with a fresh, curious goalkeeper.

const int X_PIN = A0, Y_PIN = A1;
const char DIRS[3] = {'L', 'C', 'R'};
const float MARKOV_WEIGHT = 0.6;         // how much to trust "what did they do after their last shot"

int counts[3] = {1, 1, 1};               // how often each direction was shot (start at 1 = no idea)
int after[3][3] = {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}};   // after[last][next]
int last = -1;                           // the previous shot, -1 = none yet
bool ready = true;                       // the stick must return to the middle between shots
int saves = 0, goals = 0;

// The probability of each next direction, in percent
void predict(float p[3]) {
  float total = counts[0] + counts[1] + counts[2];
  for (int d = 0; d < 3; d++) p[d] = counts[d] / total;      // what they shoot in general
  if (last >= 0) {                                           // what they shoot after their last shot
    float t = after[last][0] + after[last][1] + after[last][2];
    for (int d = 0; d < 3; d++) p[d] = (1 - MARKOV_WEIGHT) * p[d] + MARKOV_WEIGHT * after[last][d] / t;
  }
}

int readShot() {                         // a flick left, up or right; -1 = none
  int x = analogRead(X_PIN), y = analogRead(Y_PIN);
  bool centred = abs(x - 512) < 150 && abs(y - 512) < 150;
  if (centred) { ready = true; return -1; }
  if (!ready) return -1;
  int shot = -1;
  if (x < 150) shot = 0;
  else if (x > 870) shot = 2;
  else if (y < 150 || y > 870) shot = 1;  // up (or down) = straight down the middle
  if (shot >= 0) ready = false;
  return shot;
}

void setup() {
  Serial.begin(115200);
  randomSeed(analogRead(A3));
  Serial.println("ready:1");
}

void loop() {
  // 1. The keeper decides BEFORE the shot, from the history only
  float p[3];
  predict(p);
  int keeper = 0;
  for (int d = 1; d < 3; d++) if (p[d] > p[keeper] || (p[d] == p[keeper] && random(2))) keeper = d;

  // 2. Wait for the shot
  int shot = -1;
  while (shot < 0) {
    shot = readShot();
    delay(5);
  }
  bool goal = shot != keeper;
  if (goal) goals++; else saves++;

  // 3. Tell the game: shot, dive, result and what the AI believed
  Serial.print("shot:");
  Serial.print(DIRS[shot]);
  Serial.print(",keeper:");
  Serial.print(DIRS[keeper]);
  Serial.print(",goal:");
  Serial.print(goal ? 1 : 0);
  Serial.print(",pl:");
  Serial.print(round(p[0] * 100));
  Serial.print(",pc:");
  Serial.print(round(p[1] * 100));
  Serial.print(",pr:");
  Serial.println(round(p[2] * 100));

  // 4. Learn from this shot
  counts[shot]++;
  if (last >= 0) after[last][shot]++;
  last = shot;
  delay(2300);                           // let the animation play before the next shot
}

Play it

  1. Upload the sketch. Open the Serial Monitor at 115200 baud and flick the joystick left, up and right: each flick prints a line like shot:L,keeper:R,goal:1,.... Close the Serial Monitor.
  2. Open AI goalkeeper in Chrome or Edge on a computer, click Connect your board and choose the UNO's port.
  3. Flick the joystick: left, up (centre) or right. Let it spring back to the middle before the next shot. Watch the "AI's guess" bars change as it learns.
  4. Take 20 penalties trying to "be random". Then take 20 following a pattern like left, right, left, right. How quickly does the AI catch on?

How it works

The goalkeeper must dive before the ball is kicked, just like a real one, so it can only use the past. It keeps two kinds of memory. counts is simple: how often you shot left, centre and right. If you love the left corner, the keeper notices. after is a 3 × 3 table: for each of your previous shots, what did you shoot next? If you always switch sides after shooting left, that row of the table shows it. A model that predicts the next step from the step before is called a Markov model, after the mathematician Andrey Markov. Text prediction on phones started out with the same idea, using words instead of corners.

Counts become probabilities by dividing by the total: 6 shots left out of 10 is 0.6, or 60 %. The two models are blended, 60 % from the Markov table and 40 % from the overall counts, and the keeper dives to the direction with the highest probability. All counts start at 1 instead of 0, so the AI starts with "no idea" (33 % each) and never thinks something is impossible. That trick is called smoothing.

Against a truly random shooter, no keeper can save more than about a third of the shots. If the AI saves much more than that, you have a pattern, even if you didn't notice it yourself.

Try this next

  • Change MARKOV_WEIGHT to 0 and to 1. Which keeper is harder to beat?
  • Make the Markov table remember the last two shots: after[last2][last][next], 27 numbers.
  • Print the save percentage after every 10 shots. Who in your family is the most unpredictable?
  • Make the AI forget slowly: every 20 shots, halve all counts, so old habits count less than new ones.

Troubleshooting

  • A flick counts twice: let the stick return to the middle between shots. If it still happens, your joystick doesn't centre well: change 150 to 200 in readShot().
  • Up doesn't shoot: some joysticks give a high value for up and others a low one; the sketch accepts both. Check VRy on A1.
  • The game doesn't react: the UNO sends a line only after each shot. Check "Connection help" on the Playground page for the last line received.
  • "Could not connect": another program is using the port. Close the Serial Monitor and the Serial Plotter, then click Connect again.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

UNO R3 Starter Kit | Arduino IDE Compatible | Free Tutorials $56.00

Shop the kit