Skip to content
GameGenresEspañol
Sports

Sports

You play a sport or compete in races: controlling an athlete, a team or a vehicle, or running a club from the manager's office. The challenge combines the sport's rules and tactics with your reflexes or your management.

Also known as: sports game, sports video game

Defining mechanics

  • The rules of a real sport, simplified and enforced by the game, which acts as referee with the score, serves, fouls, outs and the clock.
  • Direct control of an athlete or, in team sports, of the player nearest the ball, while the AI moves your teammates and the opponent.
  • Shots, passes and throws whose outcome depends on when and in which direction you make them, not just on pressing a button.
  • Player and team attributes (speed, power, accuracy), often based on real, licensed athletes and leagues.
  • Variants with layers of their own, such as running a club, extreme sports built on tricks and combos, and racing, which forms a subgenre of its own.

Gameplay loop

Read the play, get into position, hit or pass at the right moment, watch the score change and adjust your tactics to the opponent until the end of the match.

Our demo

Controls. Up and down arrows or W/S to move your racket (the yellow one). With the mouse or a finger, the racket moves towards the pointer. When it is your serve, press space, Enter or click.

Open full screen

A top-down tennis game against an AI, with simplified real rules. There is a cross-court serve with a change of side every point, a change of server every game, the 15-30-40 count with deuce and advantage, and the ball is out if it leaves over the sidelines. Where the ball touches the racket decides where it goes. The first to win three games wins.

Made with Kaplay, in about 175 lines of JavaScript.

How it's made

  1. Setup and constants

    The game is created with virtual buttons (up, down, serve), and the court is defined along with the numbers that decide how it plays, such as the speed of the players, the speed of the ball and how much it speeds up with each hit, and AIM_SPREAD, which says how much the angle opens up when you hit with the racket's edge.

    const k = kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [46, 110, 70],
      buttons: {
        up: { keyboard: ["up", "w"] },
        down: { keyboard: ["down", "s"] },
        serve: { keyboard: ["space", "enter"] },
      },
    });
    
    // The court, seen from above: baselines left and right, sidelines top and bottom.
    const COURT = { left: 40, right: 600, top: 50, bottom: 330 };
    const MID_Y = (COURT.top + COURT.bottom) / 2;
    const HALF_H = (COURT.bottom - COURT.top) / 2;
    const RACKET_H = 56;
    const PLAYER_SPEED = 330; // px/s, the same for you and the AI
    const BALL_SPEED = 320; // first shot of a rally
    const MAX_SPEED = 560; // every hit is 5% faster, up to this
    const AIM_SPREAD = 1.25; // hitting with the racket's edge aims beyond the sideline
    const GAMES_TO_WIN = 3;
  2. The court

    The court is drawn with lines, which are the baselines, sidelines, service lines, centre line and net. The lines are not decoration, because the rules rely on them to decide whether the ball is in or out.

    onDraw(() => {
      const { left, right, top, bottom } = COURT;
      drawRect({ pos: vec2(left, top), width: right - left, height: bottom - top, color: rgb(60, 120, 190) });
      const line = (a, b) => drawLine({ p1: vec2(...a), p2: vec2(...b), width: 2, color: rgb(255, 255, 255) });
      line([left, top], [right, top]);
      line([left, bottom], [right, bottom]);
      line([left, top], [left, bottom]);
      line([right, top], [right, bottom]);
      line([170, top], [170, bottom]); // service lines
      line([470, top], [470, bottom]);
      line([170, MID_Y], [470, MID_Y]); // centre service line
      drawLine({ p1: vec2(320, top - 10), p2: vec2(320, bottom + 10), width: 4, color: rgb(230, 230, 230) }); // net
    });
  3. The players

    Each player is a racket that moves vertically, with the same top speed for you and the AI, so the match is fair. With the keyboard you move directly. With the mouse or a finger, the racket heads towards the pointer at that same speed. You can step slightly outside the court to reach wide balls.

    const racket = (x, col) => add([rect(10, RACKET_H), pos(x, MID_Y), anchor("center"), color(col), { goal: MID_Y }]);
    const players = [racket(COURT.left + 12, rgb(255, 220, 90)), racket(COURT.right - 12, rgb(255, 120, 120))];
    const [me, ai] = players;
    const moveTowards = (p, goal) => {
      p.pos.y += clamp(goal - p.pos.y, -PLAYER_SPEED * dt(), PLAYER_SPEED * dt());
      p.pos.y = clamp(p.pos.y, 20, 340); // you may step out of the court to reach wide balls
    };
    // Keyboard moves the racket; the mouse or a finger sets where it should go.
    let pointerY = null;
    onMouseMove((p) => (pointerY = p.y));
    me.onUpdate(() => {
      if (state === "serve" && server === 0) return; // the server stands still to serve
      if (isButtonDown("up") || isButtonDown("down")) {
        pointerY = null;
        me.pos.y += (isButtonDown("down") ? 1 : -1) * PLAYER_SPEED * dt();
        me.pos.y = clamp(me.pos.y, 20, 340);
      } else if (pointerY !== null) moveTowards(me, pointerY);
    });
  4. The rival AI

    The AI works out where the ball will cross its line and places the racket so the ball hits the part it wants, and that is how it aims. It reacts with a delay, like a person, and picks a safe angle at random. It also knows the rules, because it lets go balls that are flying out, since that wins it the point.

    // The AI predicts where the ball will cross its baseline and places the racket
    // so the ball hits the part it wants (that is how it aims). It reacts late,
    // like a person, and it lets go balls that are flying out: they lose the point.
    let aiDelay = 0;
    let aiAim = 0;
    ai.onUpdate(() => {
      if (state === "serve" && server === 1) return;
      aiDelay -= dt();
      let goal = MID_Y;
      if (state === "rally" && ball.vel.x > 0 && aiDelay <= 0) {
        const y = ball.pos.y + (ball.vel.y * (ai.pos.x - ball.pos.x)) / ball.vel.x;
        const goingOut = y < COURT.top - 6 || y > COURT.bottom + 6;
        if (!goingOut) goal = y - aiAim * (RACKET_H / 2);
      }
      moveTowards(ai, goal);
    });
  5. The ball and the shot

    The ball travels in a straight line towards a point on the rival's baseline. A hit happens when it crosses the racket's line within its height. The crossing is checked rather than the overlap, so a fast ball cannot pass through the racket between two frames. Where it touches decides the target, as the centre sends it down the middle while the edge opens it towards the lines. Each hit makes it 5% faster.

    const ball = add([circle(6), pos(0, 0), anchor("center"), color(230, 255, 120), { vel: vec2(0, 0), speed: BALL_SPEED }]);
    let lastHitter = 0;
    
    // Shoot the ball from where it is towards a point on the other baseline.
    function shoot(from, targetY, speed) {
      const targetX = from === 0 ? COURT.right : COURT.left;
      ball.speed = speed;
      ball.vel = vec2(targetX, targetY).sub(ball.pos).unit().scale(speed);
      lastHitter = from;
      if (from === 0) {
        aiDelay = 0.22; // reaction time
        aiAim = rand(-0.75, 0.75);
      }
    }
    
    // A hit happens when the ball crosses the racket's line within its height.
    // Where it touches the racket (centre or edge) decides the aim, as in Pong.
    function checkHit(p, who, prevX) {
      const crossed = who === 0 ? prevX >= p.pos.x && ball.pos.x < p.pos.x : prevX <= p.pos.x && ball.pos.x > p.pos.x;
      const offset = (ball.pos.y - p.pos.y) / (RACKET_H / 2 + 6);
      if (!crossed || Math.abs(offset) > 1) return;
      ball.pos.x = p.pos.x;
      shoot(who, MID_Y + offset * HALF_H * AIM_SPREAD, Math.min(MAX_SPEED, ball.speed * 1.05));
    }
    
    ball.onUpdate(() => {
      if (state === "serve") {
        const s = players[server]; // the ball waits in front of the server
        ball.pos = s.pos.add(server === 0 ? 16 : -16, 0);
        return;
      }
      if (state !== "rally") return;
      const prevX = ball.pos.x;
      ball.pos = ball.pos.add(ball.vel.scale(dt()));
      if (ball.vel.x < 0) checkHit(me, 0, prevX);
      else checkHit(ai, 1, prevX);
  6. Rules, out and serve

    This is the genre's mechanic, the game acting as referee. If the ball leaves over a sideline before it is returned, it is out and the point goes to the hitter's rival. If it crosses a baseline inside the court, whoever had to return it missed. The serve goes cross-court and the server switches sides every point, right on even points and left on odd ones, as in real tennis. Aiming at the lines is a risk the rules punish or reward.

      // Simplified tennis rules. If the ball leaves the court over a sideline
      // before anyone returns it, it is OUT: the point goes to the other player.
      // If it passes a baseline inside the court, the receiver missed it.
      const out = ball.pos.y < COURT.top || ball.pos.y > COURT.bottom;
      if (out) return endPoint(1 - lastHitter, t({ es: "¡Fuera!", en: "Out!" }));
      if (ball.pos.x < COURT.left - 10) return endPoint(1, t({ es: "Punto del rival", en: "Rival's point" }));
      if (ball.pos.x > COURT.right + 10) return endPoint(0, t({ es: "¡Punto para ti!", en: "Your point!" }));
    });
    
    // The serve goes cross-court, and the server switches side every point
    // (right side on even points, left on odd ones), as in real tennis.
    let state = "serve";
    let server = 0;
    function placeServer() {
      const played = score.points[0] + score.points[1];
      const side = played % 2 === 0 ? 1 : -1; // 1 = server's right-hand side
      players[server].pos.y = MID_Y + side * (server === 0 ? 1 : -1) * HALF_H * 0.5;
      if (server === 1) wait(1.2, () => state === "serve" && serve());
    }
    function serve() {
      state = "rally";
      const s = players[server];
      shoot(server, MID_Y - (s.pos.y - MID_Y), BALL_SPEED * 0.9); // aim at the opposite diagonal
    }
    onButtonPress("serve", () => state === "serve" && server === 0 && serve());
    onMousePress(() => state === "serve" && server === 0 && serve());
  7. Tennis scoring

    Points are counted 0, 15, 30, 40 and game. At 40 all it is «deuce», and two points in a row are needed, advantage and then game. Every game changes the server. Turning an odd count like this into a few conditions is a good example of how a video game encodes a rulebook.

    // Tennis counts 0, 15, 30, 40, game. At 40–40 it is «deuce»: you need two
    // points in a row (advantage, then game). The server changes every game.
    const score = { points: [0, 0], games: [0, 0] };
    const CALLS = ["0", "15", "30", "40"];
    function callScore() {
      const [a, b] = score.points;
      if (a >= 3 && b >= 3) {
        if (a === b) return t({ es: "Iguales", en: "Deuce" });
        return a > b ? t({ es: "Ventaja tuya", en: "Advantage you" }) : t({ es: "Ventaja rival", en: "Advantage rival" });
      }
      return `${CALLS[a]} – ${CALLS[b]}`;
    }
    function endPoint(winner, message) {
      state = "pause";
      const p = score.points;
      p[winner]++;
      if (p[winner] >= 4 && p[winner] - p[1 - winner] >= 2) {
        score.games[winner]++;
        score.points = [0, 0];
        server = 1 - server;
        message += "\n" + t({ es: "Juego", en: "Game" });
      }
      banner.text = message;
      wait(1.4, () => {
        banner.text = "";
        if (score.games[winner] === GAMES_TO_WIN) return go("end", winner === 0, score.games);
        state = "serve";
        placeServer();
      });
    }
  8. Scoreboard

    The scoreboard shows the games and the current game's count as it is called in tennis, and tells you when it is your serve. After each point a banner explains what happened (out, point, game), so the player learns the rules while playing.

    const hud = add([text("", { size: 16, font: FONT, align: "center" }), pos(320, 22), anchor("center")]);
    const banner = add([text("", { size: 26, font: FONT, align: "center" }), pos(320, 160), anchor("center"), z(10)]);
    onUpdate(() => {
      const serving = state === "serve" && server === 0 ? t({ es: "  ·  Sacas tú: espacio o clic", en: "  ·  Your serve: space or click" }) : "";
      const games = t({ es: `Tú ${score.games[0]} – ${score.games[1]} Rival`, en: `You ${score.games[0]} – ${score.games[1]} Rival` });
      hud.text = `${games}   ·   ${callScore()}${serving}`;
    });
    placeServer();
  9. Scenes

    The game has three scenes, title, game and end. The title explains the controls and the key idea of aiming with the racket's edge, and the end scene shows the result of the set.

    function screen(title, subtitle) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
      add([text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(220, 240, 220)]);
      onButtonPress("serve", () => go("game"));
      onClick(() => go("game"));
    }
    
    scene("title", () =>
      screen(
        t({ es: "Tenis", en: "Tennis" }),
        t({
          es: `Mueve tu raqueta (amarilla) con las flechas o W/S, o con el ratón o el dedo. Donde toca la bola decide hacia dónde sale: con el borde apuntas a las líneas, pero puede irse fuera. Gana ${GAMES_TO_WIN} juegos.\nEspacio o clic para empezar.`,
          en: `Move your racket (yellow) with the arrows or W/S, or with the mouse or a finger. Where the ball touches it decides where it goes: the edge aims at the lines, but it may go out. Win ${GAMES_TO_WIN} games.\nSpace or click to start.`,
        }),
      ),
    );
    
    scene("end", (won, games) =>
      screen(
        won ? t({ es: "¡Ganaste el set!", en: "You won the set!" }) : t({ es: "El rival gana el set", en: "The rival wins the set" }),
        t({ es: `Juegos ${games[0]} – ${games[1]}. Espacio o clic para jugar otra vez.`, en: `Games ${games[0]} – ${games[1]}. Space or click to play again.` }),
      ),
    );
    
    go("title");

What we simplified

  • The ball does not bounce or have height, and the net does not stop it. A shot is «in» if the ball crosses the rival's baseline inside the court, and «out» if it leaves over a sideline first.
  • The players only move vertically along their baseline, and there are no double faults, no lets on the serve, no tie-break and no change of ends.
  • The set is won with three games and without needing a two-game lead (six in real tennis), and the score always shows your points first even when the rival serves.

To modify it

  • Require a two-game lead to win the set, and add a 7-point tie-break when it is tied.
  • Add a second serve. If the first one goes out it is a fault, and two faults in a row give the point to the rival.
  • Let the players also move horizontally, to come up to the net or drop back, and think about what changes in the AI.
  • Change the sport to volleyball or badminton, deciding which lines, serves and scoring are needed.
Full code
// Sports demo (tennis)
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";

// #region setup
const k = kaplay({
  width: 640,
  height: 360,
  letterbox: true, pixelDensity,
  background: [46, 110, 70],
  buttons: {
    up: { keyboard: ["up", "w"] },
    down: { keyboard: ["down", "s"] },
    serve: { keyboard: ["space", "enter"] },
  },
});

// The court, seen from above: baselines left and right, sidelines top and bottom.
const COURT = { left: 40, right: 600, top: 50, bottom: 330 };
const MID_Y = (COURT.top + COURT.bottom) / 2;
const HALF_H = (COURT.bottom - COURT.top) / 2;
const RACKET_H = 56;
const PLAYER_SPEED = 330; // px/s, the same for you and the AI
const BALL_SPEED = 320; // first shot of a rally
const MAX_SPEED = 560; // every hit is 5% faster, up to this
const AIM_SPREAD = 1.25; // hitting with the racket's edge aims beyond the sideline
const GAMES_TO_WIN = 3;
// #endregion

scene("game", () => {
  // #region court
  onDraw(() => {
    const { left, right, top, bottom } = COURT;
    drawRect({ pos: vec2(left, top), width: right - left, height: bottom - top, color: rgb(60, 120, 190) });
    const line = (a, b) => drawLine({ p1: vec2(...a), p2: vec2(...b), width: 2, color: rgb(255, 255, 255) });
    line([left, top], [right, top]);
    line([left, bottom], [right, bottom]);
    line([left, top], [left, bottom]);
    line([right, top], [right, bottom]);
    line([170, top], [170, bottom]); // service lines
    line([470, top], [470, bottom]);
    line([170, MID_Y], [470, MID_Y]); // centre service line
    drawLine({ p1: vec2(320, top - 10), p2: vec2(320, bottom + 10), width: 4, color: rgb(230, 230, 230) }); // net
  });
  // #endregion

  // #region players
  const racket = (x, col) => add([rect(10, RACKET_H), pos(x, MID_Y), anchor("center"), color(col), { goal: MID_Y }]);
  const players = [racket(COURT.left + 12, rgb(255, 220, 90)), racket(COURT.right - 12, rgb(255, 120, 120))];
  const [me, ai] = players;
  const moveTowards = (p, goal) => {
    p.pos.y += clamp(goal - p.pos.y, -PLAYER_SPEED * dt(), PLAYER_SPEED * dt());
    p.pos.y = clamp(p.pos.y, 20, 340); // you may step out of the court to reach wide balls
  };
  // Keyboard moves the racket; the mouse or a finger sets where it should go.
  let pointerY = null;
  onMouseMove((p) => (pointerY = p.y));
  me.onUpdate(() => {
    if (state === "serve" && server === 0) return; // the server stands still to serve
    if (isButtonDown("up") || isButtonDown("down")) {
      pointerY = null;
      me.pos.y += (isButtonDown("down") ? 1 : -1) * PLAYER_SPEED * dt();
      me.pos.y = clamp(me.pos.y, 20, 340);
    } else if (pointerY !== null) moveTowards(me, pointerY);
  });
  // #endregion

  // #region ai
  // The AI predicts where the ball will cross its baseline and places the racket
  // so the ball hits the part it wants (that is how it aims). It reacts late,
  // like a person, and it lets go balls that are flying out: they lose the point.
  let aiDelay = 0;
  let aiAim = 0;
  ai.onUpdate(() => {
    if (state === "serve" && server === 1) return;
    aiDelay -= dt();
    let goal = MID_Y;
    if (state === "rally" && ball.vel.x > 0 && aiDelay <= 0) {
      const y = ball.pos.y + (ball.vel.y * (ai.pos.x - ball.pos.x)) / ball.vel.x;
      const goingOut = y < COURT.top - 6 || y > COURT.bottom + 6;
      if (!goingOut) goal = y - aiAim * (RACKET_H / 2);
    }
    moveTowards(ai, goal);
  });
  // #endregion

  // #region ball
  const ball = add([circle(6), pos(0, 0), anchor("center"), color(230, 255, 120), { vel: vec2(0, 0), speed: BALL_SPEED }]);
  let lastHitter = 0;

  // Shoot the ball from where it is towards a point on the other baseline.
  function shoot(from, targetY, speed) {
    const targetX = from === 0 ? COURT.right : COURT.left;
    ball.speed = speed;
    ball.vel = vec2(targetX, targetY).sub(ball.pos).unit().scale(speed);
    lastHitter = from;
    if (from === 0) {
      aiDelay = 0.22; // reaction time
      aiAim = rand(-0.75, 0.75);
    }
  }

  // A hit happens when the ball crosses the racket's line within its height.
  // Where it touches the racket (centre or edge) decides the aim, as in Pong.
  function checkHit(p, who, prevX) {
    const crossed = who === 0 ? prevX >= p.pos.x && ball.pos.x < p.pos.x : prevX <= p.pos.x && ball.pos.x > p.pos.x;
    const offset = (ball.pos.y - p.pos.y) / (RACKET_H / 2 + 6);
    if (!crossed || Math.abs(offset) > 1) return;
    ball.pos.x = p.pos.x;
    shoot(who, MID_Y + offset * HALF_H * AIM_SPREAD, Math.min(MAX_SPEED, ball.speed * 1.05));
  }

  ball.onUpdate(() => {
    if (state === "serve") {
      const s = players[server]; // the ball waits in front of the server
      ball.pos = s.pos.add(server === 0 ? 16 : -16, 0);
      return;
    }
    if (state !== "rally") return;
    const prevX = ball.pos.x;
    ball.pos = ball.pos.add(ball.vel.scale(dt()));
    if (ball.vel.x < 0) checkHit(me, 0, prevX);
    else checkHit(ai, 1, prevX);
    // #endregion

    // #region rules
    // Simplified tennis rules. If the ball leaves the court over a sideline
    // before anyone returns it, it is OUT: the point goes to the other player.
    // If it passes a baseline inside the court, the receiver missed it.
    const out = ball.pos.y < COURT.top || ball.pos.y > COURT.bottom;
    if (out) return endPoint(1 - lastHitter, t({ es: "¡Fuera!", en: "Out!" }));
    if (ball.pos.x < COURT.left - 10) return endPoint(1, t({ es: "Punto del rival", en: "Rival's point" }));
    if (ball.pos.x > COURT.right + 10) return endPoint(0, t({ es: "¡Punto para ti!", en: "Your point!" }));
  });

  // The serve goes cross-court, and the server switches side every point
  // (right side on even points, left on odd ones), as in real tennis.
  let state = "serve";
  let server = 0;
  function placeServer() {
    const played = score.points[0] + score.points[1];
    const side = played % 2 === 0 ? 1 : -1; // 1 = server's right-hand side
    players[server].pos.y = MID_Y + side * (server === 0 ? 1 : -1) * HALF_H * 0.5;
    if (server === 1) wait(1.2, () => state === "serve" && serve());
  }
  function serve() {
    state = "rally";
    const s = players[server];
    shoot(server, MID_Y - (s.pos.y - MID_Y), BALL_SPEED * 0.9); // aim at the opposite diagonal
  }
  onButtonPress("serve", () => state === "serve" && server === 0 && serve());
  onMousePress(() => state === "serve" && server === 0 && serve());
  // #endregion

  // #region scoring
  // Tennis counts 0, 15, 30, 40, game. At 40–40 it is «deuce»: you need two
  // points in a row (advantage, then game). The server changes every game.
  const score = { points: [0, 0], games: [0, 0] };
  const CALLS = ["0", "15", "30", "40"];
  function callScore() {
    const [a, b] = score.points;
    if (a >= 3 && b >= 3) {
      if (a === b) return t({ es: "Iguales", en: "Deuce" });
      return a > b ? t({ es: "Ventaja tuya", en: "Advantage you" }) : t({ es: "Ventaja rival", en: "Advantage rival" });
    }
    return `${CALLS[a]} – ${CALLS[b]}`;
  }
  function endPoint(winner, message) {
    state = "pause";
    const p = score.points;
    p[winner]++;
    if (p[winner] >= 4 && p[winner] - p[1 - winner] >= 2) {
      score.games[winner]++;
      score.points = [0, 0];
      server = 1 - server;
      message += "\n" + t({ es: "Juego", en: "Game" });
    }
    banner.text = message;
    wait(1.4, () => {
      banner.text = "";
      if (score.games[winner] === GAMES_TO_WIN) return go("end", winner === 0, score.games);
      state = "serve";
      placeServer();
    });
  }
  // #endregion

  // #region hud
  const hud = add([text("", { size: 16, font: FONT, align: "center" }), pos(320, 22), anchor("center")]);
  const banner = add([text("", { size: 26, font: FONT, align: "center" }), pos(320, 160), anchor("center"), z(10)]);
  onUpdate(() => {
    const serving = state === "serve" && server === 0 ? t({ es: "  ·  Sacas tú: espacio o clic", en: "  ·  Your serve: space or click" }) : "";
    const games = t({ es: `Tú ${score.games[0]} – ${score.games[1]} Rival`, en: `You ${score.games[0]} – ${score.games[1]} Rival` });
    hud.text = `${games}   ·   ${callScore()}${serving}`;
  });
  placeServer();
  // #endregion
});

// #region scenes
function screen(title, subtitle) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
  add([text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }), pos(center().add(0, 30)), anchor("center"), color(220, 240, 220)]);
  onButtonPress("serve", () => go("game"));
  onClick(() => go("game"));
}

scene("title", () =>
  screen(
    t({ es: "Tenis", en: "Tennis" }),
    t({
      es: `Mueve tu raqueta (amarilla) con las flechas o W/S, o con el ratón o el dedo. Donde toca la bola decide hacia dónde sale: con el borde apuntas a las líneas, pero puede irse fuera. Gana ${GAMES_TO_WIN} juegos.\nEspacio o clic para empezar.`,
      en: `Move your racket (yellow) with the arrows or W/S, or with the mouse or a finger. Where the ball touches it decides where it goes: the edge aims at the lines, but it may go out. Win ${GAMES_TO_WIN} games.\nSpace or click to start.`,
    }),
  ),
);

scene("end", (won, games) =>
  screen(
    won ? t({ es: "¡Ganaste el set!", en: "You won the set!" }) : t({ es: "El rival gana el set", en: "The rival wins the set" }),
    t({ es: `Juegos ${games[0]} – ${games[1]}. Espacio o clic para jugar otra vez.`, en: `Games ${games[0]} – ${games[1]}. Space or click to play again.` }),
  ),
);

go("title");
// #endregion

Design challenges

  • Choosing which rules to simulate and which to simplify, so the sport is recognisable and the match stays quick and readable.
  • A rival AI that plays like a person, with reaction time and mistakes, neither perfect nor clumsy.
  • Controlling a whole team with a single controller, which demands good player switching and teammates who position themselves sensibly.
  • Balancing realism and fun, and choosing a camera (top-down, side-on or TV-style) that lets you see the play.

History

  1. 1958
    OriginTennis for Two

    Tennis for Two (1958), by William Higinbotham, a two-player tennis game on an oscilloscope, is the earliest sports video game.

    Sports video game (Wikipedia)
  2. 1972
    Pong

    Atari's Pong, the first commercially successful video game, is a simplified table tennis.

    Sports video game (Wikipedia)
  3. 1982
    Football Manager

    Football Manager, for the ZX Spectrum, is the first association football management simulation.

    Sports video game (Wikipedia)
  4. 1983
    Track & Field

    Konami's Track & Field turns athletics into alternating two buttons as fast as possible and is one of the highest-grossing arcade games of 1984.

    Track & Field (Wikipedia)
  5. 1993
    FIFA International Soccer

    FIFA International Soccer brings the isometric perspective to football and is the best-selling home game of 1993 in the United Kingdom.

    Sports video game (Wikipedia)
  6. 1997
    Gran Turismo

    Gran Turismo, a racing simulator for the PlayStation, ships more than ten million copies.

    Gran Turismo (1997 video game) (Wikipedia)
  7. 1999
    Tony Hawk's Pro Skater

    Tony Hawk's Pro Skater popularises extreme sports, with tricks chained into combos, and its series becomes one of the most popular in sports games.

    Sports video game (Wikipedia)
  8. 2006
    Wii Sports

    Wii Sports has you play by copying the real movement with a motion controller and becomes Nintendo's best-selling game ever.

    Wii Sports (Wikipedia)
  9. 2021
    Doodle Champion Island Games

    Google celebrates the Tokyo Olympics with Doodle Champion Island Games, seven sports mini-games played free in the browser.

    Doodle Champion Island Games (Wikipedia)

Reference games

1958

Tennis for Two

William Higinbotham y Robert V. Dvorak (Laboratorio Nacional de Brookhaven). osciloscopio con computadora analógica

A two-player tennis game seen from the side on an oscilloscope screen; each controller had a knob to set the angle and a button to hit. It was shown to the public at a Brookhaven open day.

1972

Pong

Atari (Allan Alcorn). arcade

Table tennis stripped to its essentials; each player moves a paddle to return the ball and the first to reach eleven points wins. It is the first commercially successful video game.

1982

Football Manager

Kevin Toms (Addictive Games). ZX81, ZX Spectrum, Commodore 64, BBC Micro, Amstrad CPC

You do not play the matches, you run the club. You start in the fourth division, pick the line-up, buy and sell players and look after the finances to get promoted, almost all with text and the keyboard.

1983

Track & Field

Konami. arcade

Six athletics events, from the 100 metres to the high jump. You run by alternating two buttons as fast as you can and use a third one to time the jump or the throw; if you miss the qualifying mark, you lose a life.

1993

FIFA International Soccer

Extended Play Productions (EA Canada). Mega Drive

Football with national teams seen in an isometric perspective, when other football games used a top-down view. It is the first in EA Sports' FIFA series.

1997

Gran Turismo

Polys Entertainment. PlayStation

A driving simulator with 140 real cars. In its career mode you earn driving licences, win money in races and spend it buying and tuning cars.

1999

Tony Hawk's Pro Skater

Neversoft. PlayStation, Nintendo 64, Dreamcast

You skate through open levels and chain tricks into combos to score points; if you bail, you lose the combo. In career mode you have two minutes per session to complete goals.

2006

Wii Sports

Nintendo EAD. Wii

Five sports (tennis, baseball, bowling, golf and boxing) that you play by copying the real movement with the Wii's motion controller, as if it were a racket, a club or a ball.

2012

Basketball 2012 (Google Doodle)

Google. navegador

A Google Doodle for the 2012 Olympic Games in which you have 24 seconds to score as many baskets as you can; you hold and release at the right moment, and the shooter moves further from the hoop as you score.

2021

Doodle Champion Island Games

Google con Studio 4°C. navegador

A Google Doodle with seven sports mini-games (table tennis, skateboarding, archery, rugby, artistic swimming, climbing and marathon) linked by an island to explore. Each one simplifies its sport's rules down to one or two buttons.

Exercise

Play a set of the demo and make two lists, one with the real tennis rules that are there (cross-court serve, switching sides every point, 15-30-40, deuce and advantage, out) and another with the ones that were simplified. In the code, change the shot spread and the AI speed and explain how the difficulty changes. Finally, pick another net sport (volleyball, badminton) and write in half a page the minimum rules its video game version would have.

Subgenres

  • Cue sportsYou play billiards, pool, snooker or carom: aiming, choosing the strength and spin of each shot and working out how the balls will rebound. The challenge is precision and planning the next shots.
  • FishingYou fish for sport: choosing the spot, the bait and the tackle, casting the line, reacting in time when a fish bites and reeling it in without snapping the line while it pulls. You win on the weight or size of your catch.
  • Football (soccer)You control a football team during the match, switching to whichever player is nearest the ball: passing, dribbling, shooting and defending in real time. The challenge is combining with your teammates to create chances and stop the other side.
  • GolfYou play a golf course stroke by stroke: choosing a club, aiming with the wind and the terrain in mind and setting the power of the shot, often with a meter you stop at just the right moment. You win by sinking the ball in each hole in as few strokes as possible.
  • RacingA game in which you compete to finish first or in the shortest time on a circuit or a route, almost always at the wheel of a vehicle. The challenge is mastering speed and trajectory without going off the track or losing time in the corners.

Sources