Skip to main content

Game state management

The game state is managed through two primary structures and a maze array.

State structures

From src/include/misc.h:1-18:
struct pcman {
  float x, y;              // Grid position (not pixels)
  float inc_x, inc_y;      // Current direction (-1, 0, 1)
  float old_inc_x, old_inc_y; // Previous direction
  float old_x, old_y;      // Previous position
  float yf, dir;           // Calculation helpers
  int puntos;              // Dots remaining
  int puntuacion;          // Score
  int nivel;               // Level 0-13
  int tiempo;              // Power-up duration
  int estado_pcman;        // NORMAL or ENFADADO
  int num_vidas;           // Lives remaining
};

Game states

Defined in src/include/defines.h:3-5:
#define NORMAL 0     // Normal gameplay
#define ENFADADO 1   // Scared/edible (ghosts) or powered-up (Pacman)
#define MUERTO 2     // Dead (ghosts returning to base)

Initialization

The inicializa_structuras() function in src/misc.c:280-477 sets up game state.

Pacman initialization

From misc.c:282-298:
void inicializa_structuras(struct pcman *pc, struct fantasmas *fan, int value)
{
  pc->x = 7;
  pc->y = 11;
  pc->old_x = 7;
  pc->old_y = 11;
  pc->inc_x = 0;
  pc->inc_y = -1;  // Start moving up
  pc->old_inc_x = 0;
  pc->old_inc_y = 0;
  pc->estado_pcman = NORMAL;
  if (value == TRUE || value == COMENZAR) {
    pc->puntos = NUM_PUNTOS;  // 226 dots
  }
  if (value == COMENZAR) {
    pc->nivel = 0;
    pc->num_vidas = 2;
    pc->puntuacion = 0;
  }
}

Ghost initialization

Each ghost starts at a different position in the “ghost house” (center of maze):
From misc.c:300-308:
fan->who = 0;
fan->x[fan->who] = 17;
fan->y[fan->who] = 11;
fan->inc_x[fan->who] = 1;  // Moving right
fan->inc_y[fan->who] = 0;
fan->old_mov_x[fan->who] = 1;
fan->old_mov_y[fan->who] = 0;
fan->find_x[fan->who] = pc->x;
fan->find_y[fan->who] = pc->y;
From misc.c:310-318:
fan->who = 1;
fan->x[fan->who] = 19;
fan->y[fan->who] = 12;
fan->inc_x[fan->who] = 0;
fan->inc_y[fan->who] = 1;  // Moving down
// ... same pattern
Similar initialization at positions (15, 11) and (16, 14) respectively.

Velocity initialization

From misc.c:345-451, velocity is set based on level:
switch(pc->nivel) {
case 0:
  pc->tiempo = 273;  // Power-up duration (15 seconds * 18.2 Hz)
  inc_velocidad[0] = velocidades[pc->nivel];  // 0.4
  inc_velocidad[1] = velocidades[pc->nivel];
  inc_velocidad[2] = velocidades[pc->nivel];
  inc_velocidad[3] = velocidades[pc->nivel];
  break;
case 1:
  pc->tiempo = 198;  // 11 seconds
  inc_velocidad[0] = velocidades[1];  // 0.4231
  // ...
}
Velocity array from misc.c:140-155:
float velocidades[14] = {
  0.4,     // Level 0
  0.4231,  // Level 1
  0.4462,  // Level 2
  // ... increases up to
  0.7      // Level 13
};

Ghost AI system

The ghost AI is split between two files:

Target selection

From src/misc.c:760-836, each ghost has unique behavior:
Ambush behavior - Tries to predict Pacman’s positionFrom misc.c:760-780:
if (fan.estado_fantasma[0] == NORMAL) {
  if (fan.x[0] == fan.find_xp[0] && fan.y[0] == fan.find_yp[0])
  {
    fan.find_xp[0] = pc.x;
    fan.find_yp[0] = pc.y;
    
    fan.find_x[0] = fan.find_xp[0];
    fan.find_y[0] = fan.find_yp[0];
  }
} else if (fan.estado_fantasma[0] == ENFADADO) {
  // Random target when scared
  alea[0] = rand() % MAXX_A;
  alea[1] = rand() % MAXY_A;
  fan.find_x[0] = alea[0];
  fan.find_y[0] = alea[1];
} else {
  // Return to ghost house when dead
  fan.find_x[0] = 16;
  fan.find_y[0] = 12;
}

Pathfinding algorithm

The optener_movimientos_fantasmas() function in src/movefant.c:279-526 implements the core pathfinding.

Movement decision logic

From movefant.c:282-356, the algorithm considers:
  1. Previous movement direction - Stored in fan->old_mov_x and fan->old_mov_y
  2. Target position - From fan->find_x and fan->find_y
  3. Walkable cells - Checked via truex() and truey() helpers
Core logic for Y-axis movement (movefant.c:287-356):
if (fan->old_mov_y[fan->who] != 0)
{
  if (fan->x[fan->who] > fan->find_x[fan->who])
  {
    if (comprueba_distancias_x(fan))
    {
      if (truex(-1, fan))  // Can move left?
      {
        fan->inc_x[fan->who] = -1;
        fan->inc_y[fan->who] = 0;
      }
    }
    else
      devuelve_valores_aleatorios_y(fan);
  }
  else if (fan->x[fan->who] < fan->find_x[fan->who])
  {
    if (comprueba_distancias_x(fan))
    {
      if (truex(1, fan))  // Can move right?
      {
        fan->inc_x[fan->who] = 1;
        fan->inc_y[fan->who] = 0;
      }
    }
    else
      devuelve_valores_aleatorios_y(fan);
  }
}

Wall collision helpers

From movefant.c:29-41:
int truex(int value, struct fantasmas *fan)
{
  if (value == -1 &&
      c_array[(int)((fan->y[fan->who] * 32) + (fan->x[fan->who] + value))] == P)
    return 0;  // Door blocks ghosts moving left
  else
    return (c_array[(int)((fan->y[fan->who] * 32) + 
                         (fan->x[fan->who] + value))]);
}

int truey(int value, struct fantasmas *fan)
{
  return (c_array[(int)(((fan->y[fan->who] + value) * 32) + 
                        fan->x[fan->who])]);
}
Returns non-zero if the cell is walkable (C, N, J, B, or P for ghosts moving right).

Randomization

When ghosts hit a junction or dead end, devuelve_valores_aleatorios_x() and devuelve_valores_aleatorios_y() (movefant.c:121-277) add unpredictability:
void devuelve_valores_aleatorios_y(struct fantasmas *fan)
{
  int alea_t;
  fan->inc_y[fan->who] = 0;
  fan->inc_x[fan->who] = 0;

  if (truex(-1, fan) && truex(1, fan) && truey(fan->old_mov_y[fan->who], fan))
  {
    alea_t = rand() % 30;
    if (alea_t >= 0 && alea_t <= 9)
      fan->inc_x[fan->who] = 1;
    else if (alea_t >= 10 && alea_t <= 19)
      fan->inc_x[fan->who] = -1;
    else if (alea_t >= 20 && alea_t <= 29)
      fan->inc_y[fan->who] = fan->old_mov_y[fan->who];
  }
  // ... more cases
}
This creates a 33% chance for each direction when multiple paths are available.

Collision detection

From src/mov_fig.c:190-210, collision uses distance-based detection:
if (fabs((xf + j[who]) - (old_xp + jp + 1)) < 10 && 
    fabs((yf + i[who]) - (old_yp + ip)) < 10) {
  if (fan->estado_fantasma[who] == NORMAL) {
    // Pacman dies
    pc->num_vidas--;
    if (pc->num_vidas < 0)
      menu_opciones(FALSE);
    inicializa_structuras(pc, fan, FALSE);
    CntStep2 = 0;
  } else if (fan->estado_fantasma[who] == ENFADADO) {
    // Eat ghost
    switch (control_puntos_fan_muerto) {
      case 0: writef(xf, yf, vaddr, "%2d", 50 ); pc->puntuacion += 50;  break;
      case 1: writef(xf, yf, vaddr, "%3d", 100); pc->puntuacion += 100; break;
      case 2: writef(xf, yf, vaddr, "%3d", 150); pc->puntuacion += 150; break;
      case 3: writef(xf, yf, vaddr, "%3d", 200); pc->puntuacion += 200; break;
    }
    fan->estado_fantasma[who] = MUERTO;
    control_puntos_fan_muerto++;
  }
}
The collision radius is 10 pixels (roughly the size of the sprites). The control_puntos_fan_muerto variable tracks how many ghosts have been eaten in sequence for progressive scoring.

Scoring system

Dot collection

From misc.c:714-730:
if (c_array [(int)(pc.x + (pc.y * 32))] == C) {
  c_array[(int)(pc.x + (pc.y * 32))] = J;  // Mark as walked
  xp = COOR_X + (pc.x * 8) - 6;
  yp = COOR_Y + (pc.y * 8) - 8;
  putico(xp + 6, yp + 4, punto_negro, tablero->pixels, 4, 4);  // Erase dot
  pc.puntos--;
  pc.puntuacion+=10;
  if (pc.puntos == 0) {
    adelanta_nivel(&fan, &pc);
  }
}
10 points per dot.

Power pellet

From misc.c:663-675:
if (c_array [(int)(pc.x + (pc.y * 32))] == B) {
  c_array[(int)(pc.x + (pc.y * 32))] = J;
  xp = COOR_X + (pc.x * 8) - 6;
  yp = COOR_Y + (pc.y * 8) - 9;
  putico(xp + 3, yp + 2, bak_asusta, tablero->pixels, 10, 10);
  pc.estado_pcman = ENFADADO;
  if (fan.estado_fantasma[0] == NORMAL) fan.estado_fantasma[0] = ENFADADO;
  if (fan.estado_fantasma[1] == NORMAL) fan.estado_fantasma[1] = ENFADADO;
  if (fan.estado_fantasma[2] == NORMAL) fan.estado_fantasma[2] = ENFADADO;
  if (fan.estado_fantasma[3] == NORMAL) fan.estado_fantasma[3] = ENFADADO;
  pc.puntuacion+=50;
  CntStep = 0;
}
50 points per power pellet.

Ghost consumption

Progressive scoring for eating multiple ghosts in one power-up:
  • 1st ghost: 50 points
  • 2nd ghost: 100 points
  • 3rd ghost: 150 points
  • 4th ghost: 200 points

Level progression

The adelanta_nivel() function in misc.c:485-489 handles level advancement:
void adelanta_nivel(struct fantasmas *fan, struct pcman *pc)
{
  pc->nivel++;
  stargame(fan, pc, TRUE);
}
This is called when pc.puntos == 0 (all dots collected).

Level differences

From misc.c:140-155, ghost speed increases from 0.4 (level 0) to 0.7 (level 13+):
float velocidades[14] = {
  0.4, 0.4231, 0.4462, 0.4692, 0.4923, 0.5154, 0.5385,
  0.5615, 0.5846, 0.6077, 0.6308, 0.6538, 0.6769, 0.7
};
Pacman’s speed remains constant at 0.5 (misc.c:173).

Maze reset

From misc.c:491-503:
void stargame(struct fantasmas *fan, struct pcman *pc, int value)
{
  if (value == TRUE || value == COMENZAR)
    copyarray();  // Reset maze from original template

  inicializa_structuras(pc, fan, value);
  tablero = LoadImage("data/tablero.bmp", 0);
  CntStep2 = 0;
  teclado();
}
The copyarray() function (misc.c:262-267) resets the working maze:
void copyarray(void)
{
  int j;
  for (j = 0; j < MAXX_A * MAXY_A; j++)
    c_array[j] = array[j];
}

Tunnel wrapping

The maze has wraparound tunnels on the sides. From movefant.c:451-464:
if (fan->x[fan->who] == 16 && fan->y[fan->who] == 1)
{
  fan->x[fan->who] = 16;
  fan->y[fan->who] = 24;
  fan->inc_y[fan->who] = -1;
  fan->inc_x[fan->who] = 0;
}
else if (fan->x[fan->who] == 16 && fan->y[fan->who] == 24)
{
  fan->x[fan->who] = 16;
  fan->y[fan->who] = 1;
  fan->inc_y[fan->who] = 1;
  fan->inc_x[fan->who] = 0;
}
Similar logic exists for Pacman in misc.c:651-660.

Ghost house exit

Ghosts leave the starting house at staggered intervals. From misc.c:840-893:
if (busca_posiciones[0] == TRUE) {
  if (fan.x[0] == 17 && fan.y[0] == 12 && fan.estado_fantasma[0] == NORMAL)  {
    if (CntStep2 >= 50 && CntStep2 <= 100) {
      fan.find_x[0] = fan.find_xp[0] = pc.x;
      fan.find_y[0] = fan.find_yp[0] = pc.y;
      fan.inc_x[0] = 1;
      fan.inc_y[0] = 0;
    } else {
      fan.find_x[0] = 15;
      fan.find_y[0] = 12;
    }
  }
}
Exit times:
  • Blue ghost: CntStep2 50-100
  • Red ghost: CntStep2 100-150
  • Yellow ghost: CntStep2 150-200
  • Gray ghost: CntStep2 200-240
This prevents all ghosts from exiting simultaneously at game start.

Build docs developers (and LLMs) love