This project implements an AI agent for a two-player fighting game called Pixel Warrior / Zoorkhane Fighter. Two agents compete in real-time combat, each starting with 100 HP. The first agent to reduce the opponent's HP to zero wins. Each agent is an independent program that receives the current game state as JSON input and returns an action as JSON output every frame.
The project explores adversarial AI techniques, specifically Minimax search with alpha-beta pruning and heuristic evaluation functions, to build a competitive fighting game agent.
.
βββ GAMECODE-python.py # Main game loop and rendering (Pygame)
βββ fighter.py # Fighter class β handles animation, physics, agent I/O
βββ agent.py # Primary AI agent (Minimax + heuristic)
βββ random-agent.py # Secondary agent (Markov model + strategy selection)
βββ health_bar.png # HUD asset
βββ p1.png # Player 1 victory screen
βββ p2.png # Player 2 victory screen
βββ README.md
Additional required assets (backgrounds, character spritesheets, fonts, sounds) must be present in their respective folders.
The main game (GAMECODE-python.py) runs at 60 FPS using Pygame. Each frame, it calls Fighter.move() for both fighters. For AI-controlled fighters, the fighter class serializes the current game state to JSON, pipes it to the agent script via subprocess, and reads back the chosen action.
The game supports three modes, configurable via game_mode in GAMECODE-python.py:
| Mode | Description |
|---|---|
ai_vs_ai |
Both players controlled by AI agents |
player_vs_ai |
Player 1 is human, Player 2 is AI |
player_vs_player |
Both players are human |
Each agent reads a JSON object from stdin and writes a JSON action to stdout.
Input:
{
"fighter": {
"x": int,
"y": int,
"health": int,
"attacking": bool,
"attack_cooldown": [int, int],
"jump": bool,
"dash_cooldown": int
},
"opponent": {
"x": int,
"y": int,
"health": int,
"attacking": bool
},
"saved_data": {}
}Output:
{
"move": "left" | "right" | null,
"attack": 1 | 2 | null,
"jump": bool,
"dash": "left" | "right" | null,
"debug": any,
"saved_data": {}
}
saved_datapersists across frames and can be used to store any state your agent needs between calls.
The primary agent uses Minimax search with alpha-beta pruning (depth 3) to select the best action each frame.
Key components:
-
assess_position(fighter, opponent)β Heuristic evaluation function. Scores a game state based on:- Health differential (highest weight)
- Distance to opponent (attack range pressure)
- Attack cooldown readiness
- Vertical alignment (both on ground vs. one in air)
- Position on screen (penalizes corner positioning)
-
simulate_action(fighter, opponent, action, is_fighter_turn)β Simulates the effect of a single action without modifying actual game state. -
minimax(fighter, opponent, depth, is_maximizing, alpha, beta)β Standard minimax with alpha-beta pruning. The maximizing player is the agent; the minimizing player is the opponent. -
choose_best_move(fighter_info, opponent_info, saved_data)β Entry point. Evaluates all legal actions and returns the one with the highest minimax score.
Available actions considered:
MOVE_FORWARD, MOVE_BACKWARD, ATTACK, HEAVY_ATTACK, DASH_FORWARD, DASH_BACKWARD, JUMP_IN_PLACE
The secondary agent uses a Markov model to predict opponent behavior and adapts its strategy dynamically.
Key components:
- Opponent modeling: Tracks action transition probabilities to predict the opponent's next move.
- Strategy selection: Chooses between
aggressive,defensive,counter,press, orbalancedbased on health differential and opponent aggression score. - Dodge logic: With 80% probability, attempts to dodge predicted attacks via dash or jump.
- Optimal positioning: Calculates ideal distance from the opponent based on current strategy.
| Mechanic | Details |
|---|---|
| Screen size | 1000 Γ 540 px |
| Ground Y | 380 |
| Jump height | 170 (minimum Y) |
| Hitbox | 120 wide Γ 180 tall, centered on fighter |
| Attack range | < 180 px horizontal distance to connect |
| Movement speed | 5 px/frame |
| Light attack | 10 damage, 25-frame cooldown |
| Heavy attack | 20 damage, 100-frame cooldown |
| Dash | 300 px over 10 frames, 40-frame cooldown |
| Time limit | 3600 frames (60 seconds) |
pip install pygame numpypython GAMECODE-python.pyTo change which agent file is used, edit the agent1_info or agent2_info dictionaries in GAMECODE-python.py:
agent1_info = {
'enabled': True,
'language': 'python',
'path': os.path.join(os.path.dirname(__file__), 'agent.py')
}game_mode = "ai_vs_ai" # AI vs AI
# game_mode = "player_vs_ai" # Human vs AI
# game_mode = "pvp" # Human vs HumanA good heuristic balances multiple factors. The general form used in agent.py:
score = (health_diff Γ w1) + (range_advantage Γ w2) + (cooldown_bonus Γ w3) β (corner_penalty Γ w4) β ...
Weights were tuned empirically. Key insights:
- Health difference is the dominant term.
- Being in attack range with a ready cooldown is highly rewarded.
- Being cornered is strongly penalized.
- Vertical misalignment (one fighter in air, one on ground) is penalized as attacks miss.
Minimax at depth 3 with alpha-beta pruning significantly reduces the search space. The opponent is modeled conservatively (assumes it plays optimally from a subset of moves), while the agent considers its full action space.
- Each agent call must complete within 0.4 seconds or the frame is skipped.
- If the agent crashes, the fighter loses by default.
saved_datamust be JSON-serializable.- Agents do not share state β each runs as a separate subprocess.