Skip to content

Instantly share code, notes, and snippets.

@ninjadynamics
Created January 15, 2026 17:44
Show Gist options
  • Select an option

  • Save ninjadynamics/61e02d1b35763921dd1d00dce5d969bf to your computer and use it in GitHub Desktop.

Select an option

Save ninjadynamics/61e02d1b35763921dd1d00dce5d969bf to your computer and use it in GitHub Desktop.
Vibe Soccer (by Claude Opus 4.5)
// Soccer Game - CPU vs CPU with Strategic AI
// Compile: gcc soccer.c -o soccer -lraylib -lm
#include "raylib.h"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define SCREEN_WIDTH 1360
#define SCREEN_HEIGHT 720
#define FIELD_MARGIN_X 80
#define FIELD_MARGIN_Y 60
#define FIELD_WIDTH (SCREEN_WIDTH - 2 * FIELD_MARGIN_X)
#define FIELD_HEIGHT (SCREEN_HEIGHT - 2 * FIELD_MARGIN_Y)
#define FIELD_LEFT FIELD_MARGIN_X
#define FIELD_RIGHT (SCREEN_WIDTH - FIELD_MARGIN_X)
#define FIELD_TOP FIELD_MARGIN_Y
#define FIELD_BOTTOM (SCREEN_HEIGHT - FIELD_MARGIN_Y)
#define FIELD_CENTER_X (SCREEN_WIDTH / 2)
#define FIELD_CENTER_Y (SCREEN_HEIGHT / 2)
#define GOAL_WIDTH 40
#define GOAL_HEIGHT 140
#define GOAL_TOP ((SCREEN_HEIGHT - GOAL_HEIGHT) / 2)
#define GOAL_BOTTOM ((SCREEN_HEIGHT + GOAL_HEIGHT) / 2)
#define PENALTY_AREA_WIDTH 130
#define PENALTY_AREA_HEIGHT 320
#define GOAL_AREA_WIDTH 50
#define GOAL_AREA_HEIGHT 160
#define PLAYER_RADIUS 18
#define BALL_RADIUS 10
#define PLAYERS_PER_TEAM 11
#define MAX_SPEED 4.0f
#define GK_MAX_SPEED 3.2f
#define BALL_FRICTION 0.982f
#define KICK_POWER 8.0f
#define PASS_POWER 7.5f
#define SHOT_POWER 12.0f
#define CONTROL_DISTANCE 25.0f
#define MIN_TEAMMATE_DISTANCE 80.0f
#define LANE_WIDTH (FIELD_HEIGHT / 5)
#define SETPIECE_CLEAR_RADIUS 80.0f
// Reaction time settings (in seconds)
#define REACTION_TIME_MIN 0.20f
#define REACTION_TIME_MAX 0.35f
#define SEEK_WEIGHT 1.0f
#define ARRIVE_WEIGHT 1.0f
#define SEPARATION_WEIGHT 3.5f
typedef enum {
STATE_KICKOFF, STATE_PLAYING, STATE_GOAL_SCORED,
STATE_THROW_IN, STATE_CORNER, STATE_GOAL_KICK
} GameState;
typedef enum {
ROLE_GOALKEEPER, ROLE_DEFENDER, ROLE_MIDFIELDER, ROLE_ATTACKER
} PlayerRole;
typedef enum { TEAM_A = 0, TEAM_B = 1 } Team;
typedef struct {
int defenders, midfielders, attackers;
const char* name;
} Formation;
Formation formations[] = {
{4, 4, 2, "4-4-2"}, {4, 3, 3, "4-3-3"}, {3, 5, 2, "3-5-2"},
{4, 5, 1, "4-5-1"}, {3, 4, 3, "3-4-3"}, {5, 3, 2, "5-3-2"}, {5, 4, 1, "5-4-1"}
};
#define NUM_FORMATIONS 7
Vector2 Vec2Add(Vector2 a, Vector2 b) { return (Vector2){a.x + b.x, a.y + b.y}; }
Vector2 Vec2Sub(Vector2 a, Vector2 b) { return (Vector2){a.x - b.x, a.y - b.y}; }
Vector2 Vec2Scale(Vector2 v, float s) { return (Vector2){v.x * s, v.y * s}; }
float Vec2Length(Vector2 v) { return sqrtf(v.x * v.x + v.y * v.y); }
float Vec2Distance(Vector2 a, Vector2 b) { return Vec2Length(Vec2Sub(a, b)); }
Vector2 Vec2Normalize(Vector2 v) {
float len = Vec2Length(v);
return (len > 0.0001f) ? Vec2Scale(v, 1.0f / len) : (Vector2){0, 0};
}
Vector2 Vec2Truncate(Vector2 v, float max) {
float len = Vec2Length(v);
return (len > max) ? Vec2Scale(Vec2Normalize(v), max) : v;
}
float Vec2Dot(Vector2 a, Vector2 b) { return a.x * b.x + a.y * b.y; }
float ClampF(float val, float min, float max) {
return (val < min) ? min : ((val > max) ? max : val);
}
float RandomFloat(float min, float max) {
return min + (float)GetRandomValue(0, 10000) / 10000.0f * (max - min);
}
typedef struct {
Vector2 pos, vel, homePos, targetPos, facing;
Team team;
PlayerRole role;
int number, laneIndex, roleIndex;
bool hasBall;
float kickCooldown, supportTimer;
float reactionTimer; // Time until player can react to ball
} Player;
typedef struct {
Vector2 pos, vel;
Player* owner;
Team lastTouchedBy;
} Ball;
typedef struct {
Player players[2][PLAYERS_PER_TEAM];
Ball ball;
GameState state;
Team kickoffTeam, lastGoalTeam, setpieceTeam;
int scoreA, scoreB;
float stateTimer;
Vector2 setpiecePos;
Formation teamAFormation, teamBFormation;
bool offsideEnabled;
} Game;
Game game;
void ResetPositions(Team kickoffTeam);
Vector2 GetFormationPosition(Team team, int playerIndex, Formation formation);
float GetOffsideLine(Team attackingTeam);
bool IsOffside(Player* player);
Vector2 GetStrategicPosition(Player* player);
float GetLaneY(int laneIndex) {
return FIELD_TOP + (FIELD_HEIGHT / 5.0f) * (laneIndex + 0.5f);
}
void AssignLanes(Team team, Formation formation) {
int defCount = formation.defenders;
int midCount = formation.midfielders;
int atkCount = formation.attackers;
game.players[team][0].laneIndex = 2;
game.players[team][0].roleIndex = 0;
for (int i = 0; i < defCount; i++) {
Player* p = &game.players[team][1 + i];
p->roleIndex = i;
if (defCount == 3) { int lanes[] = {1, 2, 3}; p->laneIndex = lanes[i]; }
else if (defCount == 4) { int lanes[] = {0, 1, 3, 4}; p->laneIndex = lanes[i]; }
else if (defCount == 5) { p->laneIndex = i; }
}
for (int i = 0; i < midCount; i++) {
Player* p = &game.players[team][1 + defCount + i];
p->roleIndex = i;
if (midCount == 3) { int lanes[] = {1, 2, 3}; p->laneIndex = lanes[i]; }
else if (midCount == 4) { int lanes[] = {0, 1, 3, 4}; p->laneIndex = lanes[i]; }
else if (midCount == 5) { p->laneIndex = i; }
}
for (int i = 0; i < atkCount; i++) {
Player* p = &game.players[team][1 + defCount + midCount + i];
p->roleIndex = i;
if (atkCount == 1) { p->laneIndex = 2; }
else if (atkCount == 2) { int lanes[] = {1, 3}; p->laneIndex = lanes[i]; }
else if (atkCount == 3) { int lanes[] = {1, 2, 3}; p->laneIndex = lanes[i]; }
}
}
// Trigger reaction time for all players (called when ball changes state)
void TriggerReactionTime(Player* excludePlayer) {
for (int t = 0; t < 2; t++) {
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[t][i];
if (p != excludePlayer && p->reactionTimer <= 0) {
p->reactionTimer = RandomFloat(REACTION_TIME_MIN, REACTION_TIME_MAX);
}
}
}
}
void InitGame(void) {
game.scoreA = 0;
game.scoreB = 0;
game.state = STATE_KICKOFF;
game.kickoffTeam = GetRandomValue(0, 1) == 0 ? TEAM_A : TEAM_B;
game.offsideEnabled = true;
game.teamAFormation = formations[GetRandomValue(0, NUM_FORMATIONS - 1)];
game.teamBFormation = formations[GetRandomValue(0, NUM_FORMATIONS - 1)];
game.ball.pos = (Vector2){FIELD_CENTER_X, FIELD_CENTER_Y};
game.ball.vel = (Vector2){0, 0};
game.ball.owner = NULL;
game.ball.lastTouchedBy = game.kickoffTeam;
for (int t = 0; t < 2; t++) {
Team team = (Team)t;
Formation form = (team == TEAM_A) ? game.teamAFormation : game.teamBFormation;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[t][i];
p->team = team;
p->number = i + 1;
p->hasBall = false;
p->kickCooldown = 0;
p->supportTimer = RandomFloat(0, 3.0f);
p->reactionTimer = 0;
if (i == 0) p->role = ROLE_GOALKEEPER;
else if (i <= form.defenders) p->role = ROLE_DEFENDER;
else if (i <= form.defenders + form.midfielders) p->role = ROLE_MIDFIELDER;
else p->role = ROLE_ATTACKER;
p->homePos = GetFormationPosition(team, i, form);
p->pos = p->homePos;
p->targetPos = p->homePos;
p->vel = (Vector2){0, 0};
p->facing = (team == TEAM_A) ? (Vector2){1, 0} : (Vector2){-1, 0};
}
AssignLanes(team, form);
}
ResetPositions(game.kickoffTeam);
}
Vector2 GetFormationPosition(Team team, int playerIndex, Formation formation) {
float x, y;
int defCount = formation.defenders;
int midCount = formation.midfielders;
int atkCount = formation.attackers;
float gkZone = 0.05f, defZone = 0.18f, midZone = 0.40f, atkZone = 0.72f;
if (playerIndex == 0) { x = gkZone; y = 0.5f; }
else if (playerIndex <= defCount) {
int idx = playerIndex - 1;
x = defZone;
y = 0.08f + (0.84f / (defCount + 1)) * (idx + 1);
} else if (playerIndex <= defCount + midCount) {
int idx = playerIndex - defCount - 1;
x = midZone;
y = 0.08f + (0.84f / (midCount + 1)) * (idx + 1);
} else {
int idx = playerIndex - defCount - midCount - 1;
x = atkZone;
y = 0.15f + (0.7f / (atkCount + 1)) * (idx + 1);
}
if (team == TEAM_A) return (Vector2){FIELD_LEFT + x * FIELD_WIDTH, FIELD_TOP + y * FIELD_HEIGHT};
else return (Vector2){FIELD_RIGHT - x * FIELD_WIDTH, FIELD_TOP + y * FIELD_HEIGHT};
}
void ResetPositions(Team kickoffTeam) {
game.ball.pos = (Vector2){FIELD_CENTER_X, FIELD_CENTER_Y};
game.ball.vel = (Vector2){0, 0};
game.ball.owner = NULL;
for (int t = 0; t < 2; t++) {
Team team = (Team)t;
Formation form = (team == TEAM_A) ? game.teamAFormation : game.teamBFormation;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[t][i];
p->homePos = GetFormationPosition(team, i, form);
p->pos = p->homePos;
p->targetPos = p->homePos;
p->vel = (Vector2){0, 0};
p->hasBall = false;
p->kickCooldown = 0;
p->reactionTimer = 0;
if (team == kickoffTeam && p->role == ROLE_ATTACKER) {
p->pos.x = (team == TEAM_A) ? FIELD_CENTER_X - 30 : FIELD_CENTER_X + 30;
}
if (team != kickoffTeam) {
if (team == TEAM_A && p->pos.x > FIELD_CENTER_X - PLAYER_RADIUS)
p->pos.x = FIELD_CENTER_X - PLAYER_RADIUS - 10;
if (team == TEAM_B && p->pos.x < FIELD_CENTER_X + PLAYER_RADIUS)
p->pos.x = FIELD_CENTER_X + PLAYER_RADIUS + 10;
}
}
}
}
Vector2 Seek(Player* player, Vector2 target) {
Vector2 desired = Vec2Normalize(Vec2Sub(target, player->pos));
float maxSpeed = (player->role == ROLE_GOALKEEPER) ? GK_MAX_SPEED : MAX_SPEED;
return Vec2Sub(Vec2Scale(desired, maxSpeed), player->vel);
}
Vector2 Arrive(Player* player, Vector2 target, float slowRadius) {
Vector2 toTarget = Vec2Sub(target, player->pos);
float dist = Vec2Length(toTarget);
if (dist < 1.0f) return (Vector2){0, 0};
float maxSpeed = (player->role == ROLE_GOALKEEPER) ? GK_MAX_SPEED : MAX_SPEED;
float speed = (dist < slowRadius) ? maxSpeed * (dist / slowRadius) : maxSpeed;
return Vec2Sub(Vec2Scale(Vec2Normalize(toTarget), speed), player->vel);
}
Vector2 SeparationFromTeammates(Player* player) {
Vector2 steering = {0, 0};
Team team = player->team;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* other = &game.players[team][i];
if (other == player) continue;
float dist = Vec2Distance(player->pos, other->pos);
if (dist < MIN_TEAMMATE_DISTANCE && dist > 0.1f) {
Vector2 diff = Vec2Normalize(Vec2Sub(player->pos, other->pos));
float strength = (MIN_TEAMMATE_DISTANCE - dist) / MIN_TEAMMATE_DISTANCE;
steering = Vec2Add(steering, Vec2Scale(diff, strength * 3.0f));
}
}
return steering;
}
float GetSpaceValue(Vector2 pos, Team myTeam) {
float minOppDist = 9999;
Team oppTeam = (myTeam == TEAM_A) ? TEAM_B : TEAM_A;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
float dist = Vec2Distance(pos, game.players[oppTeam][i].pos);
if (dist < minOppDist) minOppDist = dist;
}
return minOppDist;
}
float GetOffsideLine(Team attackingTeam) {
Team defendingTeam = (attackingTeam == TEAM_A) ? TEAM_B : TEAM_A;
float defenders[PLAYERS_PER_TEAM];
for (int i = 0; i < PLAYERS_PER_TEAM; i++)
defenders[i] = game.players[defendingTeam][i].pos.x;
for (int i = 0; i < PLAYERS_PER_TEAM - 1; i++) {
for (int j = i + 1; j < PLAYERS_PER_TEAM; j++) {
bool swap = (attackingTeam == TEAM_A) ? (defenders[i] < defenders[j]) : (defenders[i] > defenders[j]);
if (swap) { float t = defenders[i]; defenders[i] = defenders[j]; defenders[j] = t; }
}
}
float offsideLine = defenders[1];
if (attackingTeam == TEAM_A && game.ball.pos.x > offsideLine) offsideLine = game.ball.pos.x;
else if (attackingTeam == TEAM_B && game.ball.pos.x < offsideLine) offsideLine = game.ball.pos.x;
if (attackingTeam == TEAM_A && offsideLine < FIELD_CENTER_X) offsideLine = FIELD_CENTER_X;
else if (attackingTeam == TEAM_B && offsideLine > FIELD_CENTER_X) offsideLine = FIELD_CENTER_X;
return offsideLine;
}
bool IsOffside(Player* player) {
if (player->role == ROLE_GOALKEEPER) return false;
float offsideLine = GetOffsideLine(player->team);
if (player->team == TEAM_A) return player->pos.x > offsideLine + 5;
else return player->pos.x < offsideLine - 5;
}
Vector2 GetStrategicPosition(Player* player) {
Team team = player->team;
Team oppTeam = (team == TEAM_A) ? TEAM_B : TEAM_A;
Vector2 ballPos = game.ball.pos;
bool teamHasBall = (game.ball.owner != NULL && game.ball.owner->team == team);
bool iHaveBall = (game.ball.owner == player);
float attackDir = (team == TEAM_A) ? 1.0f : -1.0f;
float laneY = GetLaneY(player->laneIndex);
Vector2 basePos = player->homePos;
Vector2 targetPos = basePos;
if (player->role == ROLE_GOALKEEPER) {
float gkLineX = (team == TEAM_A) ? FIELD_LEFT + 30 : FIELD_RIGHT - 30;
float targetY = ClampF(ballPos.y, GOAL_TOP + 20, GOAL_BOTTOM - 20);
targetPos = (Vector2){gkLineX, targetY};
float distToBall = Vec2Distance(player->pos, ballPos);
if (distToBall < 180) {
targetPos.x += attackDir * (180 - distToBall) / 180.0f * 40.0f;
}
}
else if (teamHasBall && !iHaveBall) {
if (player->role == ROLE_ATTACKER) {
float runX = ballPos.x + attackDir * 180;
runX = ClampF(runX, FIELD_LEFT + 100, FIELD_RIGHT - 100);
float runY = laneY;
player->supportTimer += GetFrameTime();
if (player->supportTimer > 2.0f) {
if (player->roleIndex % 2 == 0) {
runY = (player->laneIndex < 2) ? FIELD_TOP + 90 : FIELD_BOTTOM - 90;
} else {
float wave = sinf(player->supportTimer * 1.5f) * 80.0f;
runY = FIELD_CENTER_Y + wave;
}
if (player->supportTimer > 5.0f) player->supportTimer = 0;
}
float offsideLine = GetOffsideLine(team);
if (team == TEAM_A && runX > offsideLine - 25) runX = offsideLine - 25;
else if (team == TEAM_B && runX < offsideLine + 25) runX = offsideLine + 25;
targetPos = (Vector2){runX, ClampF(runY, FIELD_TOP + 70, FIELD_BOTTOM - 70)};
}
else if (player->role == ROLE_MIDFIELDER) {
float supportX;
if (player->roleIndex % 3 == 0) supportX = ballPos.x + attackDir * 120;
else if (player->roleIndex % 3 == 1) supportX = ballPos.x - attackDir * 80;
else supportX = ballPos.x;
supportX = ClampF(supportX, FIELD_LEFT + 150, FIELD_RIGHT - 150);
float supportY = laneY * 0.7f + ballPos.y * 0.3f;
if (player->laneIndex == 0 || player->laneIndex == 4) supportY = laneY;
targetPos = (Vector2){supportX, ClampF(supportY, FIELD_TOP + 70, FIELD_BOTTOM - 70)};
}
else if (player->role == ROLE_DEFENDER) {
float defX = basePos.x + attackDir * 30;
if (team == TEAM_A) defX = ClampF(defX, FIELD_LEFT + 80, FIELD_LEFT + FIELD_WIDTH * 0.35f);
else defX = ClampF(defX, FIELD_RIGHT - FIELD_WIDTH * 0.35f, FIELD_RIGHT - 80);
targetPos = (Vector2){defX, laneY};
}
}
else if (!teamHasBall) {
if (player->role == ROLE_DEFENDER) {
float defLineX;
if (team == TEAM_A) {
defLineX = fminf(ballPos.x - 150, FIELD_LEFT + FIELD_WIDTH * 0.30f);
defLineX = ClampF(defLineX, FIELD_LEFT + 80, FIELD_LEFT + FIELD_WIDTH * 0.32f);
} else {
defLineX = fmaxf(ballPos.x + 150, FIELD_RIGHT - FIELD_WIDTH * 0.30f);
defLineX = ClampF(defLineX, FIELD_RIGHT - FIELD_WIDTH * 0.32f, FIELD_RIGHT - 80);
}
float markY = laneY;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* opp = &game.players[oppTeam][i];
if (opp->role == ROLE_ATTACKER) {
if (fabsf(opp->pos.y - laneY) < LANE_WIDTH * 0.8f) {
markY = opp->pos.y * 0.6f + laneY * 0.4f;
break;
}
}
}
targetPos = (Vector2){defLineX, ClampF(markY, FIELD_TOP + 70, FIELD_BOTTOM - 70)};
}
else if (player->role == ROLE_MIDFIELDER) {
float midLineX = (team == TEAM_A) ? ballPos.x - 130 : ballPos.x + 130;
if (team == TEAM_A) midLineX = ClampF(midLineX, FIELD_LEFT + 200, FIELD_CENTER_X + 80);
else midLineX = ClampF(midLineX, FIELD_CENTER_X - 80, FIELD_RIGHT - 200);
float midY = laneY * 0.6f + ballPos.y * 0.4f;
targetPos = (Vector2){midLineX, ClampF(midY, FIELD_TOP + 70, FIELD_BOTTOM - 70)};
}
else if (player->role == ROLE_ATTACKER) {
float atkX = (team == TEAM_A) ?
ClampF(ballPos.x + 80, FIELD_CENTER_X - 50, FIELD_RIGHT - 150) :
ClampF(ballPos.x - 80, FIELD_LEFT + 150, FIELD_CENTER_X + 50);
targetPos = (Vector2){atkX, laneY};
}
}
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* mate = &game.players[team][i];
if (mate == player || mate->role == ROLE_GOALKEEPER) continue;
float dist = Vec2Distance(targetPos, mate->targetPos);
if (dist < MIN_TEAMMATE_DISTANCE && dist > 0.1f) {
Vector2 away = Vec2Normalize(Vec2Sub(targetPos, mate->targetPos));
away.x *= 1.5f;
away = Vec2Normalize(away);
targetPos = Vec2Add(targetPos, Vec2Scale(away, (MIN_TEAMMATE_DISTANCE - dist) * 0.7f));
}
float horizDist = fabsf(targetPos.x - mate->targetPos.x);
if (horizDist < 50 && player->role == mate->role) {
float pushDir = (targetPos.x > mate->targetPos.x) ? 1.0f : -1.0f;
targetPos.x += pushDir * (50 - horizDist) * 0.5f;
}
}
targetPos.x = ClampF(targetPos.x, FIELD_LEFT + 40, FIELD_RIGHT - 40);
targetPos.y = ClampF(targetPos.y, FIELD_TOP + 40, FIELD_BOTTOM - 40);
return targetPos;
}
Player* GetBestPassTarget(Player* passer) {
Player* best = NULL;
float bestScore = -1000;
Team team = passer->team;
Team oppTeam = (team == TEAM_A) ? TEAM_B : TEAM_A;
float attackDir = (team == TEAM_A) ? 1.0f : -1.0f;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* target = &game.players[team][i];
if (target == passer || target->role == ROLE_GOALKEEPER) continue;
float dist = Vec2Distance(passer->pos, target->pos);
if (dist < 70 || dist > 400) continue;
bool clear = true;
Vector2 passDir = Vec2Normalize(Vec2Sub(target->pos, passer->pos));
for (int j = 0; j < PLAYERS_PER_TEAM && clear; j++) {
Player* opp = &game.players[oppTeam][j];
Vector2 toOpp = Vec2Sub(opp->pos, passer->pos);
float proj = Vec2Dot(toOpp, passDir);
if (proj > 30 && proj < dist - 30) {
Vector2 closest = Vec2Add(passer->pos, Vec2Scale(passDir, proj));
if (Vec2Distance(closest, opp->pos) < 40) clear = false;
}
}
if (!clear) continue;
if (game.offsideEnabled && IsOffside(target)) continue;
float score = 0;
float forwardProgress = (target->pos.x - passer->pos.x) * attackDir;
score += forwardProgress * 0.5f;
score += GetSpaceValue(target->pos, team) * 0.5f;
if (target->role == ROLE_ATTACKER) score += 60;
else if (target->role == ROLE_MIDFIELDER) score += 30;
if (forwardProgress > 50) score += 40;
if (fabsf(target->pos.y - passer->pos.y) > 200) score += 50;
if (dist > 280) score -= 30;
if (score > bestScore) { bestScore = score; best = target; }
}
return best;
}
bool CanShoot(Player* player) {
Team oppTeam = (player->team == TEAM_A) ? TEAM_B : TEAM_A;
float goalX = (player->team == TEAM_A) ? FIELD_RIGHT : FIELD_LEFT;
float distToGoal = Vec2Distance(player->pos, (Vector2){goalX, FIELD_CENTER_Y});
// Increased shooting range
if (distToGoal > 350) return false;
for (int angle = -1; angle <= 1; angle++) {
Vector2 target = {goalX, FIELD_CENTER_Y + angle * 50};
Vector2 shotDir = Vec2Normalize(Vec2Sub(target, player->pos));
float shotDist = Vec2Distance(player->pos, target);
bool clear = true;
for (int i = 0; i < PLAYERS_PER_TEAM && clear; i++) {
Player* opp = &game.players[oppTeam][i];
Vector2 toOpp = Vec2Sub(opp->pos, player->pos);
float proj = Vec2Dot(toOpp, shotDir);
if (proj > 0 && proj < shotDist) {
Vector2 closest = Vec2Add(player->pos, Vec2Scale(shotDir, proj));
if (Vec2Distance(closest, opp->pos) < 30) clear = false;
}
}
if (clear) return true;
}
return false;
}
// Ensure goalkeeper facing is always away from own goal
void UpdateGoalkeeperFacing(Player* player) {
if (player->role != ROLE_GOALKEEPER) return;
float attackDir = (player->team == TEAM_A) ? 1.0f : -1.0f;
// Calculate facing direction - always pointing away from own goal
// but can vary within 180 degree arc in front of goal
Vector2 toBall = Vec2Sub(game.ball.pos, player->pos);
float ballAngle = atan2f(toBall.y, toBall.x);
// Clamp to forward-facing hemisphere
if (player->team == TEAM_A) {
// Team A faces right, angle should be between -PI/2 and PI/2
if (ballAngle > 3.14159f / 2.0f) ballAngle = 3.14159f / 2.0f;
if (ballAngle < -3.14159f / 2.0f) ballAngle = -3.14159f / 2.0f;
} else {
// Team B faces left, angle should be between PI/2 and 3PI/2
if (ballAngle > 0 && ballAngle < 3.14159f / 2.0f) ballAngle = 3.14159f / 2.0f;
if (ballAngle < 0 && ballAngle > -3.14159f / 2.0f) ballAngle = -3.14159f / 2.0f;
}
player->facing = (Vector2){cosf(ballAngle), sinf(ballAngle)};
player->facing = Vec2Normalize(player->facing);
// Ensure primary direction is correct
if ((player->team == TEAM_A && player->facing.x < 0) ||
(player->team == TEAM_B && player->facing.x > 0)) {
player->facing.x = attackDir;
}
}
void UpdatePlayerAI(Player* player) {
Vector2 steering = {0, 0};
float maxSpeed = (player->role == ROLE_GOALKEEPER) ? GK_MAX_SPEED : MAX_SPEED;
Team team = player->team;
Team oppTeam = (team == TEAM_A) ? TEAM_B : TEAM_A;
float attackDir = (team == TEAM_A) ? 1.0f : -1.0f;
float oppGoalX = (team == TEAM_A) ? FIELD_RIGHT : FIELD_LEFT;
Vector2 ballPos = game.ball.pos;
float distToBall = Vec2Distance(player->pos, ballPos);
bool iHaveBall = (game.ball.owner == player);
// Update timers
if (player->kickCooldown > 0) player->kickCooldown -= GetFrameTime();
if (player->reactionTimer > 0) player->reactionTimer -= GetFrameTime();
player->targetPos = GetStrategicPosition(player);
// Update goalkeeper facing
if (player->role == ROLE_GOALKEEPER) {
UpdateGoalkeeperFacing(player);
}
if (player->role == ROLE_GOALKEEPER) {
steering = Vec2Add(steering, Vec2Scale(Arrive(player, player->targetPos, 15), ARRIVE_WEIGHT));
// Goalkeeper can react faster
if (distToBall < 60 && game.ball.owner != player && player->kickCooldown <= 0) {
steering = Vec2Add(steering, Vec2Scale(Seek(player, ballPos), SEEK_WEIGHT * 2.5f));
}
}
else if (iHaveBall) {
float distToGoal = Vec2Distance(player->pos, (Vector2){oppGoalX, FIELD_CENTER_Y});
int pressure = 0;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
if (Vec2Distance(player->pos, game.players[oppTeam][i].pos) < 65) pressure++;
}
// MORE AGGRESSIVE SHOOTING - shoot more often and from further
bool shouldShoot = CanShoot(player) && (
distToGoal < 280 || // Always try from close range
(distToGoal < 350 && player->role == ROLE_ATTACKER) || // Attackers shoot from further
(distToGoal < 320 && pressure >= 2) // Under pressure, take a shot
);
if (shouldShoot) {
float targetY = FIELD_CENTER_Y + GetRandomValue(-55, 55);
Vector2 shotDir = Vec2Normalize(Vec2Sub((Vector2){oppGoalX, targetY}, player->pos));
game.ball.vel = Vec2Scale(shotDir, SHOT_POWER + (player->role == ROLE_ATTACKER ? 1.0f : 0));
game.ball.owner = NULL;
player->hasBall = false;
player->kickCooldown = 0.5f;
TriggerReactionTime(player);
}
else if (pressure >= 2 || (pressure >= 1 && player->role == ROLE_DEFENDER)) {
Player* passTarget = GetBestPassTarget(player);
if (passTarget != NULL) {
Vector2 leadPos = Vec2Add(passTarget->pos, Vec2Scale(passTarget->vel, 10));
Vector2 passDir = Vec2Normalize(Vec2Sub(leadPos, player->pos));
float power = fminf(PASS_POWER * (1.0f + Vec2Distance(player->pos, passTarget->pos) / 350.0f), PASS_POWER * 1.5f);
game.ball.vel = Vec2Scale(passDir, power);
game.ball.owner = NULL;
player->hasBall = false;
player->kickCooldown = 0.3f;
TriggerReactionTime(player);
} else {
steering = Vec2Add(steering, Vec2Scale(Seek(player, Vec2Add(player->pos, (Vector2){attackDir * 80, 0})), SEEK_WEIGHT));
}
}
else {
Vector2 dribbleTarget = {player->pos.x + attackDir * 120, player->pos.y};
float bestY = player->pos.y, bestSpace = 0;
for (int yOff = -100; yOff <= 100; yOff += 50) {
Vector2 testPos = {dribbleTarget.x, ClampF(player->pos.y + yOff, FIELD_TOP + 80, FIELD_BOTTOM - 80)};
float space = GetSpaceValue(testPos, team);
if (space > bestSpace) { bestSpace = space; bestY = testPos.y; }
}
dribbleTarget.y = bestY;
dribbleTarget.x = ClampF(dribbleTarget.x, FIELD_LEFT + 70, FIELD_RIGHT - 70);
steering = Vec2Add(steering, Vec2Scale(Seek(player, dribbleTarget), SEEK_WEIGHT * 0.9f));
// Random pass/shot decision - attackers more likely to shoot
int shootChance = (player->role == ROLE_ATTACKER) ? 8 : 3;
if (GetRandomValue(0, 100) < shootChance && CanShoot(player)) {
float targetY = FIELD_CENTER_Y + GetRandomValue(-55, 55);
Vector2 shotDir = Vec2Normalize(Vec2Sub((Vector2){oppGoalX, targetY}, player->pos));
game.ball.vel = Vec2Scale(shotDir, SHOT_POWER);
game.ball.owner = NULL;
player->hasBall = false;
player->kickCooldown = 0.5f;
TriggerReactionTime(player);
}
else if (GetRandomValue(0, 100) < 3) {
Player* passTarget = GetBestPassTarget(player);
if (passTarget != NULL && GetRandomValue(0, 100) < 40) {
Vector2 passDir = Vec2Normalize(Vec2Sub(passTarget->pos, player->pos));
game.ball.vel = Vec2Scale(passDir, PASS_POWER);
game.ball.owner = NULL;
player->hasBall = false;
player->kickCooldown = 0.3f;
TriggerReactionTime(player);
}
}
}
}
else {
// REACTION TIME CHECK - player can't chase ball if still reacting
bool canReact = (player->reactionTimer <= 0);
int closerCount = 0;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* mate = &game.players[team][i];
if (mate == player || mate->role == ROLE_GOALKEEPER) continue;
if (Vec2Distance(mate->pos, ballPos) < distToBall - 25) closerCount++;
}
bool shouldChase = canReact && (closerCount < 2) && (player->role != ROLE_GOALKEEPER);
if (player->role == ROLE_DEFENDER) {
bool ballInDefThird = (team == TEAM_A) ?
(ballPos.x < FIELD_LEFT + FIELD_WIDTH * 0.4f) :
(ballPos.x > FIELD_RIGHT - FIELD_WIDTH * 0.4f);
shouldChase = shouldChase && ballInDefThird;
}
if (shouldChase) {
Vector2 interceptPos = (game.ball.owner == NULL) ?
Vec2Add(ballPos, Vec2Scale(game.ball.vel, 6)) : ballPos;
steering = Vec2Add(steering, Vec2Scale(Seek(player, interceptPos), SEEK_WEIGHT * 1.4f));
} else {
steering = Vec2Add(steering, Vec2Scale(Arrive(player, player->targetPos, 40), ARRIVE_WEIGHT));
}
}
steering = Vec2Add(steering, Vec2Scale(SeparationFromTeammates(player), SEPARATION_WEIGHT));
player->vel = Vec2Add(player->vel, Vec2Scale(steering, 0.12f));
player->vel = Vec2Truncate(player->vel, maxSpeed);
// Update facing for non-goalkeepers
if (player->role != ROLE_GOALKEEPER && Vec2Length(player->vel) > 0.5f) {
Vector2 newFacing = Vec2Normalize(player->vel);
player->facing.x = player->facing.x * 0.85f + newFacing.x * 0.15f;
player->facing.y = player->facing.y * 0.85f + newFacing.y * 0.15f;
player->facing = Vec2Normalize(player->facing);
}
player->pos = Vec2Add(player->pos, player->vel);
// Allow players to go outside the field (for throw-ins, etc.) but not too far
player->pos.x = ClampF(player->pos.x, FIELD_LEFT - 40, FIELD_RIGHT + 40);
player->pos.y = ClampF(player->pos.y, FIELD_TOP - 40, FIELD_BOTTOM + 40);
if (player->role == ROLE_GOALKEEPER) {
float gkMinX = (team == TEAM_A) ? FIELD_LEFT - 10 : FIELD_RIGHT - PENALTY_AREA_WIDTH;
float gkMaxX = (team == TEAM_A) ? FIELD_LEFT + PENALTY_AREA_WIDTH : FIELD_RIGHT + 10;
player->pos.x = ClampF(player->pos.x, gkMinX, gkMaxX);
}
// Ball control - REACTION TIME CHECK
bool canControlBall = (player->reactionTimer <= 0) || (player->role == ROLE_GOALKEEPER);
if (game.ball.owner == NULL && distToBall < CONTROL_DISTANCE && player->kickCooldown <= 0 && canControlBall) {
if (game.offsideEnabled && game.ball.lastTouchedBy != team && IsOffside(player)) {
game.state = STATE_GOAL_KICK;
game.setpieceTeam = (team == TEAM_A) ? TEAM_B : TEAM_A;
game.setpiecePos = (game.setpieceTeam == TEAM_A) ?
(Vector2){FIELD_LEFT + GOAL_AREA_WIDTH, FIELD_CENTER_Y} :
(Vector2){FIELD_RIGHT - GOAL_AREA_WIDTH, FIELD_CENTER_Y};
game.stateTimer = 1.0f;
return;
}
game.ball.owner = player;
player->hasBall = true;
game.ball.lastTouchedBy = team;
if (player->role != ROLE_GOALKEEPER) {
player->facing = (team == TEAM_A) ? (Vector2){1, 0} : (Vector2){-1, 0};
}
TriggerReactionTime(player);
}
// Goalkeeper grab
if (player->role == ROLE_GOALKEEPER && game.ball.owner != NULL && game.ball.owner->team != team) {
if (Vec2Distance(player->pos, game.ball.owner->pos) < PLAYER_RADIUS * 2 + BALL_RADIUS) {
game.ball.owner->hasBall = false;
game.ball.owner = player;
player->hasBall = true;
game.ball.lastTouchedBy = team;
UpdateGoalkeeperFacing(player);
Player* clearTarget = NULL;
float bestScore = -9999;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* mate = &game.players[team][i];
if (mate == player) continue;
float dist = Vec2Distance(player->pos, mate->pos);
if (dist < 100 || dist > 400) continue;
// Only pass to players in front of goal (away from own goal)
float mateDir = (mate->pos.x - player->pos.x) * attackDir;
if (mateDir < 0) continue; // Don't pass backwards toward own goal
float score = (mate->role == ROLE_DEFENDER) ? 100 : ((mate->role == ROLE_MIDFIELDER) ? 60 : 20);
score -= dist * 0.08f;
score += GetSpaceValue(mate->pos, team) * 0.3f;
if (score > bestScore) { bestScore = score; clearTarget = mate; }
}
if (clearTarget != NULL) {
game.ball.vel = Vec2Scale(Vec2Normalize(Vec2Sub(clearTarget->pos, player->pos)), PASS_POWER * 1.3f);
} else {
// Boot it upfield - never toward own goal
game.ball.vel = (Vector2){attackDir * KICK_POWER * 1.6f, GetRandomValue(-40, 40) * 0.06f};
}
game.ball.owner = NULL;
player->hasBall = false;
player->kickCooldown = 0.5f;
TriggerReactionTime(player);
}
}
}
void UpdateBall(void) {
if (game.ball.owner != NULL) {
Player* owner = game.ball.owner;
// For goalkeeper, ensure ball is always in front (away from own goal)
if (owner->role == ROLE_GOALKEEPER) {
UpdateGoalkeeperFacing(owner);
}
game.ball.pos = Vec2Add(owner->pos, Vec2Scale(owner->facing, PLAYER_RADIUS + BALL_RADIUS + 2));
game.ball.vel = (Vector2){0, 0};
} else {
game.ball.vel = Vec2Scale(game.ball.vel, BALL_FRICTION);
game.ball.pos = Vec2Add(game.ball.pos, game.ball.vel);
}
// Check goals first
if (game.ball.pos.x < FIELD_LEFT && game.ball.pos.y > GOAL_TOP && game.ball.pos.y < GOAL_BOTTOM) {
game.scoreB++; game.lastGoalTeam = TEAM_B; game.state = STATE_GOAL_SCORED;
game.kickoffTeam = TEAM_A; game.stateTimer = 2.0f;
game.ball.vel = (Vector2){0, 0}; game.ball.owner = NULL; return;
}
if (game.ball.pos.x > FIELD_RIGHT && game.ball.pos.y > GOAL_TOP && game.ball.pos.y < GOAL_BOTTOM) {
game.scoreA++; game.lastGoalTeam = TEAM_A; game.state = STATE_GOAL_SCORED;
game.kickoffTeam = TEAM_B; game.stateTimer = 2.0f;
game.ball.vel = (Vector2){0, 0}; game.ball.owner = NULL; return;
}
// Sideline - throw in
if (game.ball.pos.y < FIELD_TOP || game.ball.pos.y > FIELD_BOTTOM) {
game.state = STATE_THROW_IN;
game.setpieceTeam = (game.ball.lastTouchedBy == TEAM_A) ? TEAM_B : TEAM_A;
game.setpiecePos.x = ClampF(game.ball.pos.x, FIELD_LEFT + 5, FIELD_RIGHT - 5);
game.setpiecePos.y = (game.ball.pos.y < FIELD_CENTER_Y) ? FIELD_TOP : FIELD_BOTTOM;
game.stateTimer = 1.0f;
game.ball.owner = NULL;
game.ball.vel = (Vector2){0, 0};
return;
}
// Goal line
if (game.ball.pos.x < FIELD_LEFT) {
if (game.ball.lastTouchedBy == TEAM_A) {
game.state = STATE_GOAL_KICK; game.setpieceTeam = TEAM_A;
game.setpiecePos = (Vector2){FIELD_LEFT + GOAL_AREA_WIDTH, FIELD_CENTER_Y};
} else {
game.state = STATE_CORNER; game.setpieceTeam = TEAM_B;
game.setpiecePos = (Vector2){FIELD_LEFT + 5, (game.ball.pos.y < FIELD_CENTER_Y) ? FIELD_TOP + 5 : FIELD_BOTTOM - 5};
}
game.stateTimer = 1.0f; game.ball.owner = NULL;
game.ball.vel = (Vector2){0, 0};
return;
}
if (game.ball.pos.x > FIELD_RIGHT) {
if (game.ball.lastTouchedBy == TEAM_B) {
game.state = STATE_GOAL_KICK; game.setpieceTeam = TEAM_B;
game.setpiecePos = (Vector2){FIELD_RIGHT - GOAL_AREA_WIDTH, FIELD_CENTER_Y};
} else {
game.state = STATE_CORNER; game.setpieceTeam = TEAM_A;
game.setpiecePos = (Vector2){FIELD_RIGHT - 5, (game.ball.pos.y < FIELD_CENTER_Y) ? FIELD_TOP + 5 : FIELD_BOTTOM - 5};
}
game.stateTimer = 1.0f; game.ball.owner = NULL;
game.ball.vel = (Vector2){0, 0};
return;
}
}
void HandleSetPiece(void) {
game.stateTimer -= GetFrameTime();
// Find the taker first
float minDist = 9999;
Player* taker = NULL;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[game.setpieceTeam][i];
if (p->role == ROLE_GOALKEEPER && game.state != STATE_GOAL_KICK) continue;
float dist = Vec2Distance(p->pos, game.setpiecePos);
if (dist < minDist) { minDist = dist; taker = p; }
}
// Calculate taker position based on set piece type
Vector2 takerOffset = {0, 0};
Vector2 takerFacing = {1, 0};
if (game.state == STATE_THROW_IN) {
// Throw-in: taker stands OUTSIDE the sideline, facing INTO the field
float outsideOffset = PLAYER_RADIUS + BALL_RADIUS + 8;
if (game.setpiecePos.y <= FIELD_TOP) {
// Top sideline - taker above the line, facing down (into field)
takerOffset = (Vector2){0, -outsideOffset};
takerFacing = (Vector2){0, 1};
} else {
// Bottom sideline - taker below the line, facing up (into field)
takerOffset = (Vector2){0, outsideOffset};
takerFacing = (Vector2){0, -1};
}
} else if (game.state == STATE_CORNER) {
// Corner: taker outside corner, angled toward goal
float attackDir = (game.setpieceTeam == TEAM_A) ? 1.0f : -1.0f;
takerOffset = (Vector2){-attackDir * (PLAYER_RADIUS + BALL_RADIUS + 5), 0};
takerFacing = (Vector2){attackDir, 0};
} else {
// Goal kick and others: behind the ball, facing forward
float attackDir = (game.setpieceTeam == TEAM_A) ? 1.0f : -1.0f;
takerOffset = (Vector2){-attackDir * (PLAYER_RADIUS + BALL_RADIUS + 5), 0};
takerFacing = (Vector2){attackDir, 0};
}
Vector2 takerPos = Vec2Add(game.setpiecePos, takerOffset);
// Move ONLY OPPONENTS away from the TAKER
if (taker != NULL) {
Team oppTeam = (game.setpieceTeam == TEAM_A) ? TEAM_B : TEAM_A;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[oppTeam][i];
float dist = Vec2Distance(p->pos, takerPos);
if (dist < SETPIECE_CLEAR_RADIUS) {
Vector2 awayDir = Vec2Normalize(Vec2Sub(p->pos, takerPos));
if (Vec2Length(awayDir) < 0.1f) {
// Default push direction - into the field
awayDir = (Vector2){0, (game.setpiecePos.y < FIELD_CENTER_Y) ? 1.0f : -1.0f};
}
p->pos = Vec2Add(takerPos, Vec2Scale(awayDir, SETPIECE_CLEAR_RADIUS + 5));
// Keep opponents inside the field
p->pos.x = ClampF(p->pos.x, FIELD_LEFT + 20, FIELD_RIGHT - 20);
p->pos.y = ClampF(p->pos.y, FIELD_TOP + 20, FIELD_BOTTOM - 20);
}
}
}
if (game.stateTimer <= 0) {
if (taker != NULL) {
taker->pos = takerPos;
taker->facing = takerFacing;
taker->kickCooldown = 0.5f;
game.ball.lastTouchedBy = game.setpieceTeam;
if (game.state == STATE_THROW_IN) {
// THROW-IN: Immediately throw ball into the field - don't take possession!
// Find best teammate to throw to
Player* throwTarget = NULL;
float bestScore = -9999;
Team team = game.setpieceTeam;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* mate = &game.players[team][i];
if (mate == taker || mate->role == ROLE_GOALKEEPER) continue;
float dist = Vec2Distance(taker->pos, mate->pos);
if (dist < 60 || dist > 300) continue;
// Must be inside the field
if (mate->pos.x < FIELD_LEFT + 30 || mate->pos.x > FIELD_RIGHT - 30) continue;
if (mate->pos.y < FIELD_TOP + 30 || mate->pos.y > FIELD_BOTTOM - 30) continue;
float score = 100 - dist * 0.3f;
score += GetSpaceValue(mate->pos, team) * 0.5f;
if (score > bestScore) {
bestScore = score;
throwTarget = mate;
}
}
Vector2 throwDir;
float throwPower = PASS_POWER * 1.1f;
if (throwTarget != NULL) {
throwDir = Vec2Normalize(Vec2Sub(throwTarget->pos, taker->pos));
} else {
// No target found - just throw into the field
throwDir = takerFacing;
// Add some horizontal component toward opponent goal
float attackDir = (game.setpieceTeam == TEAM_A) ? 1.0f : -1.0f;
throwDir.x += attackDir * 0.5f;
throwDir = Vec2Normalize(throwDir);
}
// Ball starts at the sideline and is thrown in
game.ball.pos = game.setpiecePos;
game.ball.vel = Vec2Scale(throwDir, throwPower);
game.ball.owner = NULL; // Ball is in the air, no one owns it
taker->hasBall = false;
}
else {
// CORNER, GOAL KICK, etc: Take possession normally (ball is inside field)
game.ball.pos = game.setpiecePos;
game.ball.vel = (Vector2){0, 0};
game.ball.owner = taker;
taker->hasBall = true;
taker->reactionTimer = 0;
}
}
game.state = STATE_PLAYING;
TriggerReactionTime(taker);
}
}
void UpdateGame(void) {
switch (game.state) {
case STATE_KICKOFF:
game.stateTimer -= GetFrameTime();
if (game.stateTimer <= 0) {
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[game.kickoffTeam][i];
if (p->role == ROLE_ATTACKER && Vec2Distance(p->pos, game.ball.pos) < 60) {
game.ball.owner = p; p->hasBall = true;
game.ball.lastTouchedBy = game.kickoffTeam;
game.state = STATE_PLAYING;
TriggerReactionTime(p);
break;
}
}
if (game.state == STATE_KICKOFF) game.stateTimer = 0.5f;
}
for (int t = 0; t < 2; t++)
for (int i = 0; i < PLAYERS_PER_TEAM; i++)
UpdatePlayerAI(&game.players[t][i]);
UpdateBall();
break;
case STATE_PLAYING:
for (int t = 0; t < 2; t++)
for (int i = 0; i < PLAYERS_PER_TEAM; i++)
UpdatePlayerAI(&game.players[t][i]);
UpdateBall();
break;
case STATE_GOAL_SCORED:
game.stateTimer -= GetFrameTime();
if (game.stateTimer <= 0) {
ResetPositions(game.kickoffTeam);
game.state = STATE_KICKOFF;
game.stateTimer = 1.0f;
}
break;
case STATE_THROW_IN:
case STATE_CORNER:
case STATE_GOAL_KICK:
HandleSetPiece();
break;
}
}
void DrawField(void) {
DrawRectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, DARKGREEN);
DrawRectangle(FIELD_LEFT, FIELD_TOP, FIELD_WIDTH, FIELD_HEIGHT, (Color){34, 139, 34, 255});
for (int i = 0; i < 12; i++)
if (i % 2 == 0)
DrawRectangle(FIELD_LEFT + i * (FIELD_WIDTH / 12), FIELD_TOP, FIELD_WIDTH / 12, FIELD_HEIGHT, (Color){30, 130, 30, 255});
DrawRectangleLinesEx((Rectangle){FIELD_LEFT, FIELD_TOP, FIELD_WIDTH, FIELD_HEIGHT}, 3, WHITE);
DrawLine(FIELD_CENTER_X, FIELD_TOP, FIELD_CENTER_X, FIELD_BOTTOM, WHITE);
DrawCircleLines(FIELD_CENTER_X, FIELD_CENTER_Y, 60, WHITE);
DrawCircle(FIELD_CENTER_X, FIELD_CENTER_Y, 5, WHITE);
DrawRectangleLinesEx((Rectangle){FIELD_LEFT, FIELD_CENTER_Y - PENALTY_AREA_HEIGHT/2, PENALTY_AREA_WIDTH, PENALTY_AREA_HEIGHT}, 2, WHITE);
DrawRectangleLinesEx((Rectangle){FIELD_RIGHT - PENALTY_AREA_WIDTH, FIELD_CENTER_Y - PENALTY_AREA_HEIGHT/2, PENALTY_AREA_WIDTH, PENALTY_AREA_HEIGHT}, 2, WHITE);
DrawRectangleLinesEx((Rectangle){FIELD_LEFT, FIELD_CENTER_Y - GOAL_AREA_HEIGHT/2, GOAL_AREA_WIDTH, GOAL_AREA_HEIGHT}, 2, WHITE);
DrawRectangleLinesEx((Rectangle){FIELD_RIGHT - GOAL_AREA_WIDTH, FIELD_CENTER_Y - GOAL_AREA_HEIGHT/2, GOAL_AREA_WIDTH, GOAL_AREA_HEIGHT}, 2, WHITE);
DrawCircle(FIELD_LEFT + 90, FIELD_CENTER_Y, 4, WHITE);
DrawCircle(FIELD_RIGHT - 90, FIELD_CENTER_Y, 4, WHITE);
DrawCircleSector((Vector2){FIELD_LEFT, FIELD_TOP}, 15, 0, 90, 20, WHITE);
DrawCircleSector((Vector2){FIELD_RIGHT, FIELD_TOP}, 15, 90, 180, 20, WHITE);
DrawCircleSector((Vector2){FIELD_LEFT, FIELD_BOTTOM}, 15, 270, 360, 20, WHITE);
DrawCircleSector((Vector2){FIELD_RIGHT, FIELD_BOTTOM}, 15, 180, 270, 20, WHITE);
DrawRectangle(FIELD_LEFT - GOAL_WIDTH, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT, GRAY);
DrawRectangleLinesEx((Rectangle){FIELD_LEFT - GOAL_WIDTH, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT}, 2, DARKGRAY);
DrawRectangle(FIELD_RIGHT, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT, GRAY);
DrawRectangleLinesEx((Rectangle){FIELD_RIGHT, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT}, 2, DARKGRAY);
}
void DrawPlayers(void) {
for (int t = 0; t < 2; t++) {
Color teamColor = (t == TEAM_A) ? BLUE : RED;
for (int i = 0; i < PLAYERS_PER_TEAM; i++) {
Player* p = &game.players[t][i];
DrawCircle(p->pos.x, p->pos.y, PLAYER_RADIUS, teamColor);
DrawCircleLines(p->pos.x, p->pos.y, PLAYER_RADIUS, BLACK);
const char* numStr = TextFormat("%d", p->number);
int fontSize = 14;
DrawText(numStr, p->pos.x - MeasureText(numStr, fontSize)/2, p->pos.y - fontSize/2, fontSize, WHITE);
if (p->hasBall) DrawCircleLines(p->pos.x, p->pos.y, PLAYER_RADIUS + 4, YELLOW);
}
}
}
void DrawBall(void) {
DrawCircle(game.ball.pos.x, game.ball.pos.y, BALL_RADIUS, WHITE);
DrawCircleLines(game.ball.pos.x, game.ball.pos.y, BALL_RADIUS, BLACK);
}
void DrawHUD(void) {
DrawRectangle(SCREEN_WIDTH/2 - 100, 5, 200, 40, (Color){0, 0, 0, 180});
const char* scoreText = TextFormat("%d - %d", game.scoreA, game.scoreB);
DrawText(scoreText, SCREEN_WIDTH/2 - MeasureText(scoreText, 30)/2, 10, 30, WHITE);
DrawCircle(SCREEN_WIDTH/2 - 70, 25, 10, BLUE);
DrawCircle(SCREEN_WIDTH/2 + 70, 25, 10, RED);
DrawText(TextFormat("A: %s", game.teamAFormation.name), 10, 10, 16, BLUE);
const char* formB = TextFormat("B: %s", game.teamBFormation.name);
DrawText(formB, SCREEN_WIDTH - MeasureText(formB, 16) - 10, 10, 16, RED);
const char* stateText = "";
switch (game.state) {
case STATE_KICKOFF: stateText = "KICK OFF"; break;
case STATE_GOAL_SCORED: stateText = (game.lastGoalTeam == TEAM_A) ? "GOAL! BLUE SCORES!" : "GOAL! RED SCORES!"; break;
case STATE_THROW_IN: stateText = "THROW IN"; break;
case STATE_CORNER: stateText = "CORNER KICK"; break;
case STATE_GOAL_KICK: stateText = "GOAL KICK"; break;
default: break;
}
if (strlen(stateText) > 0) {
int stateWidth = MeasureText(stateText, 24);
DrawRectangle(SCREEN_WIDTH/2 - stateWidth/2 - 10, SCREEN_HEIGHT - 45, stateWidth + 20, 35, (Color){0, 0, 0, 180});
DrawText(stateText, SCREEN_WIDTH/2 - stateWidth/2, SCREEN_HEIGHT - 40, 24, (game.state == STATE_GOAL_SCORED) ? YELLOW : WHITE);
}
DrawText("R: Restart | ESC: Quit", 10, SCREEN_HEIGHT - 25, 16, (Color){255, 255, 255, 150});
}
int main(void) {
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Soccer - CPU vs CPU");
SetTargetFPS(60);
InitGame();
game.stateTimer = 1.5f;
while (!WindowShouldClose()) {
if (IsKeyPressed(KEY_R)) { InitGame(); game.stateTimer = 1.5f; }
UpdateGame();
BeginDrawing();
ClearBackground(DARKGREEN);
DrawField();
DrawPlayers();
DrawBall();
DrawHUD();
EndDrawing();
}
CloseWindow();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment