Skip to main content

How Multiplayer Works

Mokepon features a real-time multiplayer system where players can see each other moving across the map and engage in battles. The game supports both human players and CPU-controlled opponents, creating a dynamic and engaging gameplay experience.
The multiplayer server runs on Heroku at https://mokepon-ed1d40aff3a6.herokuapp.com and handles all real-time game state synchronization.

Player Session System

Joining the Game

When a player starts the game, they connect to the server via the /join endpoint:
app.get("/join", (req, res, next) => {
  try {
    const id = `${Math.random()}`;
    const player = new Player(id);
    players.push(player);

    // If there are no CPU Mokepons, generate them
    if (cpuMokepons.length === 0) {
      generateCPUMokepons();
    }

    res.json({ success: true, id });
  } catch (error) {
    next(error);
  }
});
Each player receives a unique session ID that identifies them throughout their game session. The server maintains an array of all active players, including both human and CPU opponents.
When the first player joins, the server automatically spawns CPU opponents if none exist, ensuring there are always enemies to battle.

Player Class Structure

Players are represented on the server using a Player class:
class Player {
  constructor(id) {
    this.id = id;
    // Store positions as normalized values (percentages of the map size)
    this.xPercent = undefined;
    this.yPercent = undefined;
    this.mokepon = null;
    this.attacks = [];
    this.isCPU = false; // Flag to identify CPU players
  }
}
Key Properties:
  • id: Unique identifier for each player
  • xPercent/yPercent: Normalized position coordinates (0-100% of map size)
  • mokepon: The selected Mokepon for this player
  • attacks: Array of available attacks
  • isCPU: Boolean flag distinguishing CPU from human players

Real-Time Position Updates

Position Broadcasting

The multiplayer system uses a percentage-based position system to ensure consistent rendering across different screen sizes. When a player moves, their position is sent to the server:
app.post("/mokepon/:playerId/position", (req, res) => {
  const playerId = req.params.playerId || "";
  const { xPercent, yPercent } = req.body || {};
  const playerIndex = players.findIndex((player) => player.id === playerId);

  if (playerIndex >= 0) {
    players[playerIndex].updateMokeponPosition(xPercent, yPercent);
  }

  // Filter to include both human and CPU players as enemies
  const enemies = players
    .filter(
      (player) =>
        player.id !== playerId &&
        player.mokepon &&
        player.xPercent !== undefined &&
        player.yPercent !== undefined
    )
    .map((player) => {
      return {
        id: player.id,
        xPercent: player.xPercent,
        yPercent: player.yPercent,
        mokepon: player.mokepon,
        isCPU: player.isCPU,
      };
    });

  res.send({ enemies });
});

Position Update Response

The server responds with an array of all other players (enemies), including their positions, Mokepon data, and CPU status. This allows each client to render all visible players on their map.

Safe Spawn Positioning

To prevent players from spawning too close to each other, the server provides a safe position endpoint:
app.get("/mokepon/:playerId/safePosition", (req, res) => {
  const safeDistancePercent = 20; // 20% of the map
  let safePosition = false;
  let attempts = 0;
  let xPercent, yPercent;

  while (!safePosition && attempts < 20) {
    xPercent = Math.random() * 95;
    yPercent = Math.random() * 95;

    safePosition = true;
    for (const player of players) {
      if (player.isTooCloseToPlayer(tempPlayer, safeDistancePercent)) {
        safePosition = false;
        break;
      }
    }
    attempts++;
  }

  res.send({ xPercent, yPercent, safe: safePosition });
});
Safe Positioning Algorithm:
  1. Generates random coordinates between 0-95% of map size
  2. Checks if position is at least 20% map distance from other players
  3. Makes up to 20 attempts to find a safe spawn location
  4. Returns the position and whether it’s confirmed safe

Encounter Mechanics

Collision Detection

Players can encounter each other on the map when they get close enough. The distance calculation uses percentage-based coordinates:
isTooCloseToPlayer(otherPlayer, safeDistancePercent) {
  if (
    this.xPercent === undefined ||
    this.yPercent === undefined ||
    otherPlayer.xPercent === undefined ||
    otherPlayer.yPercent === undefined
  ) {
    return false;
  }

  // Calculate distance using percentage values
  const distance = Math.sqrt(
    Math.pow(this.xPercent - otherPlayer.xPercent, 2) +
      Math.pow(this.yPercent - otherPlayer.yPercent, 2)
  );

  return distance < safeDistancePercent;
}
The collision detection uses Euclidean distance calculation on normalized coordinates, making it resolution-independent and working consistently across different devices.

Battle Initiation

When players collide on the map:
  1. The client detects the collision using the isTooCloseToPlayer method
  2. Battle interface is triggered automatically
  3. Both players’ attack sequences are loaded from the server
  4. Battle logic executes based on the rock-paper-scissors style combat system

CPU vs Human Opponents

The multiplayer system treats CPU and human opponents differently in several ways:

Human Players

  • Full control over movement
  • Manual attack selection
  • Can leave/rejoin games
  • Real-time position updates

CPU Opponents

  • Fixed spawn positions
  • Automated attack selection
  • Persistent until no humans remain
  • Regenerate when all players leave

Identifying Opponent Type

The server includes the isCPU flag in position updates, allowing clients to:
  • Display different visual indicators for CPU vs human opponents
  • Apply different battle logic for CPU encounters
  • Show appropriate UI messages (“Challenging CPU” vs “Challenging Player”)

Player Lifecycle

Session Duration

A player’s session lasts from when they join until they disconnect:
app.delete("/player/:playerId", (req, res) => {
  const playerIndex = players.findIndex((player) => player.id === playerId);

  if (playerIndex >= 0) {
    console.log(`Removing player ${playerId}`);
    players.splice(playerIndex, 1);

    // Check if we need to regenerate CPU Mokepons
    checkAndRegenerateCPUs();

    res.status(200).send({ 
      success: true, 
      message: "Player removed successfully" 
    });
  }
});

CPU Regeneration

When all human players disconnect, the server automatically regenerates CPU opponents:
function checkAndRegenerateCPUs() {
  if (!areHumanPlayersActive() && cpuMokepons.length > 0) {
    console.log("No human players left. Regenerating CPU Mokepons.");

    // Remove existing CPU players from players array
    const humanPlayers = players.filter((player) => !player.isCPU);
    players.length = 0;
    players.push(...humanPlayers);

    // Generate new CPU Mokepons
    generateCPUMokepons();
  }
}
This regeneration system ensures that each new player session starts with fresh CPU opponents in different positions with potentially different Mokepons.

Technical Details

Server Configuration

The multiplayer server uses:
  • Express.js for HTTP handling
  • CORS with production/development modes
  • Compression for response optimization
  • Helmet for security headers

Production Setup

const corsOptions = {
  origin:
    process.env.NODE_ENV === "production"
      ? "https://mokepon-ed1d40aff3a6.herokuapp.com"
      : "*",
  methods: ["GET", "POST", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
};
In production, CORS is restricted to the Heroku domain. In development, all origins are allowed for easier testing.

API Endpoints Summary

EndpointMethodPurpose
/joinGETCreate new player session
/mokepon/:playerIdPOSTAssign Mokepon to player
/mokepon/:playerId/positionPOSTUpdate position & get enemies
/mokepon/:playerId/safePositionGETGet safe spawn location
/mokepon/:playerId/attacksGET/POSTGet/set player attacks
/mokepon/:playerId/cpu-attackGETGet CPU’s next attack
/player/:playerIdDELETERemove player from game
All position coordinates are stored as percentages (0-100) rather than absolute pixels, making the system responsive and device-agnostic.

Build docs developers (and LLMs) love