Technical Explanation on Human.exe
Warning
This article is generated by MiniMax-M2.5.
Human.exe11 https://github.com/EklipZgit/generals-bot is a purely algorithmic AI for generals.io22 https://generals.io, a multiplayer web game where players compete to capture enemy generals while protecting their own. The bot does not use machine learning but relies on graph theory, combinatorial optimization, and heuristic search.
Key Features:
- Minimum Spanning Tree (MST) based gathering optimization
- Knapsack-based expansion planning
- Enemy general location prediction using BFS into fog of war
- Army interception and defense planning
- Board topology analysis (chokes, zones, central defense points)
- Win condition analysis for strategic decision making
Architecture
Project Structure
generals-bot/
├── bot_ek0x45.py # Main bot class (~16k lines)
├── BotHost.py # Game host / connection handler
├── MapMatrix.py # Efficient 2D tile storage
├── Path.py # Path representation
│
├── Gather/ # Gather algorithms
│ ├── GatherMaxIterative.py
│ ├── GatherPrune.py
│ ├── GatherSteiner.py
│ └── ...
│
├── Algorithms/
│ ├── TileIslandBuilder.py
│ ├── MapSpanningUtils.py
│ └── ...
│
├── Behavior/
│ └── ArmyInterceptor.py # Army interception logic
│
├── Strategy/
│ ├── OpponentTracker.py
│ └── WinConditionAnalyzer.py
│
└── base/ # Game client library
└── client/
└── generals.py # WebSocket client
Bot Flow
- BotHost.py receives game state updates from the server
- Calls
make_move()with the current map state - EklipZBot.find_move() is invoked, which calls
select_move() -
select_move() runs the full decision pipeline:
init_turn()- Initialize turn stateperform_move_prep()- Scan map for leaf moves, large tilespick_move_after_prep()- Main decision logic
Core Data Structures
Tile
Represents a single cell on the map:
x,y- Coordinatesplayer- Owner (-1 = neutral)army- Army count (positive = owned, negative = neutral)isCity- Whether this is a cityisGeneral- Whether this is a generaldiscovered- Fog of war statusvisible- Currently visiblemovable- Adjacent tiles reachable
MapBase
Represents the game state:
turn- Current turn numbercols,rows- Map dimensionsplayers- List of Player objectsgenerals- Dict of player_index -> general tilecities- List of city tiles
Path
Represents a sequence of moves:
start- Starting tile (PathNode)tail- End tile (PathNode)length- Number of movestileList- List of tilestileSet- Set of tiles (fast lookup)value- Strategic value of the path
Move
Represents a single move:
source- Source tiledest- Destination tilemove_half- Whether to move only half the army
Main Decision Loop
select_move() - Entry Point (bot_ek0x45.py:1885)
def select_move(self, is_lag_move=False) -> Move | None:
self.init_turn() # Initialize turn state
self.perform_move_prep() # Scan map for move candidates
return self.pick_move_after_prep() # Make decision
init_turn() - Turn Initialization (bot_ek0x45.py:894)
- Clear per-turn state
- Update board analysis (chokes, zones)
- Run army tracker scan
- Recalculate enemy general prediction
- Update tile islands
- Run opponent tracker analysis
- Scan city analyzer
- Build win condition analysis
perform_move_prep() - Move Preparation
- Find all leaf moves - tiles bordering enemy/neutral territory
- Find all capture leaf moves - leaf moves that can capture
- Identify large player tiles - high army tiles for expansion
- Update expansion plan if needed
pick_move_after_prep() - Decision Logic (bot_ek0x45.py:1941)
Priority order (top wins):
- King Kill Moves - Can capture enemy general immediately
- Defense Moves - Under immediate threat
- Army Bonus Timing - Maximize army on bonus turns
- Quick City Capture - Fast city captures in early game
- First 25 Moves - Early game expansion
- FFA Turtle Move - Defensive play in multi-player games
- Flank Defense - Protect against flanking attacks
- Enemy City Quick Kill - Take vulnerable enemy cities
- Expansion Moves - Planned expansion paths
- Gather Moves - Default economic play
Gather System
Overview
The gather system builds Minimum Spanning Trees (MST) from owned tiles to maximize army value over time. The bot can gather from multiple disconnected regions simultaneously.
Key Components
GatherTreeNode
tile- The tile being gatheredvalue- Army value gathered from this subtreeturns- Turns to reach this tilevaluePerTurn-value / turnschildren- Child GatherTreeNodes
GatherPrune.py
Prunes MST to fit within turn constraints using heap-based removal of lowest value-per-turn leaves:
def prune_mst_until(root_node, max_turns):
while root_node.turns > max_turns:
# Remove leaf with lowest valuePerTurn
remove_lowest_value_leaf(root_node)
Gather Algorithms
GatherMaxIterative.py
Iterative knapsack approach:
- Find best path to each start tile
- Use knapsack to select paths within turn limit
- Extend tree iteratively, pruning low-value branches
- Handle both positive (friendly) and negative (enemy) tiles
GatherSteiner.py
Prize-Collecting Steiner Tree (PCST) approach:
- Build graph from map tiles
- Use
pcst_fastlibrary33 https://github.com/fraenkel-lab/pcst_fast for Steiner tree optimization - Use gradient descent to find optimal prize/cost cutoff
- Returns connected tile set maximizing gathered value
Key Parameters
GATHER_SWITCH_POINT- Max tiles before switching algorithmsgather_include_distance_from_enemy_TILES_as_negatives- Penalty for gathering near enemies
Expansion System
Overview
Expansion finds the optimal set of attack paths to capture enemy territory. Uses knapsack-style optimization to balance value vs. turn cost.
TileIslandBuilder.py
Groups connected tiles into islands for strategic analysis:
- Determines which tiles can be recaptured together
- Used to avoid expanding into corners with no retreat path
IterativeExpansion.py
3-phase expansion approach:
- Large tile plans - Prioritize high-value expansion targets
- Small tile expansion - Fill gaps between large targets
- Leaf move inclusion - Add border tiles that can capture
Key Parameters
expansion_single_iteration_time_cap- Time limit per searchexpansion_enemy_expansion_plan_inbound_penalty- Penalty for expanding into enemy attack path
Army Analysis & Interception
ArmyAnalyzer.py
Analyzes spatial relationships between two points (army to army, general to enemy):
class ArmyAnalyzer:
pathWays: MapMatrix # All tiles on shortest paths
chokeWidths: Dict # Number of alternate routes per tile
interceptTurns: int # Turns to intercept (best case)
interceptDistances: int # Worst-case intercept turns
Key Algorithm:
- BFS from both points simultaneously
- Find all tiles where
dist(A) + dist(B) == shortest_distance - These form the “pathway” - all tiles that could be on a shortest path
Choke Points
- Inner chokes - Tiles with only 1 path in/out from general
- Outer chokes - Tiles where the enemy must pass through to reach general
- Choke width indicates defensibility (width=1 is most defensible)
ArmyInterceptor.py
Plans interceptions against enemy armies:
class ThreatBlockInfo:
tile: Tile # Blocking tile
army_needed: int # Army required to block
class InterceptionOptionInfo:
intercept_point: Tile # Where to intercept
damage_blocked: int # Army + tiles + recapture cost
intercept_turns: int # Turns to reach intercept
Algorithm:
- For each enemy army, find intercept points using ArmyAnalyzer
- Calculate timing windows (best vs worst case based on chokes)
- Compute damage blocked = enemy army + tiles + recapture cost
- Rank by econ value per turn
Board Analysis
BoardAnalyzer.py
Analyzes overall board topology between generals:
class BoardAnalyzer:
inner_chokes: List[Tile] # Chokes near our general
outer_chokes: List[Tile] # Chokes near enemy
central_defense_point: Tile # Best defense position
intergeneral_analysis: ArmyAnalyzer
zones: MapMatrix # Core vs extended vs flank zones
Zone Classification
- Core tiles - Behind inner chokes, very safe
- Extended tiles - Past inner chokes but before front
- Flank-danger tiles - At edges, vulnerable to flanking
Central Defense Point
Tile that minimizes sum of distances to all friendly cities and general. Used as a strategic launch point.
Enemy General Prediction
The bot uses a sophisticated system to predict where enemy generals are located. The prediction is based on analyzing where enemy armies emerge from fog of war - the logic being that armies must originate from somewhere, and that “somewhere” is likely near the general.
Overview
The prediction works by:
- Tracking emergences - Detecting when enemy armies appear from fog
- Back-propagating probability - Scoring fog tiles based on their distance from emergence points
- Accumulating evidence - Multiple emergences build up a “heat map” of likely general locations
- Constraining possibilities - Eliminating tiles that couldn’t possibly be the general
Step 1: Detection of Army Emergence
When an enemy army tile changes in a way that indicates an army moved from fog into visible territory, the bot detects this as an emergence.
How Emergence is Detected
In bot_ek0x45.py, the init_turn() method processes army emergences:
for tile, emergenceTuple in self._map.army_emergences.items():
emergedAmount, emergingPlayer = emergenceTuple
if emergedAmount > 0 or not self._map.is_player_on_team_with(tile.delta.oldOwner, tile.player):
self.opponent_tracker.notify_emerged_army(tile, emergingPlayer, emergedAmount)
This triggers OpponentTracker.notify_emerged_army() (Strategy/OpponentTracker.py:506) which queues the emergence for processing.
Step 2: Processing the Emergence
When an army can’t be traced to a known source in fog, the ArmyTracker.new_army_emerged() method (ArmyTracker.py:960) is called.
Key Algorithm (new_army_emerged):
def new_army_emerged(self, emergedTile: Tile, armyEmergenceValue: float, emergingPlayer: int = -1, distance: int | None = None):
# Distance defaults based on game turn
if distance is None:
distance = max(10, self.min_spawn_distance) + 1
if self.map.turn < 300:
distance = max(distance, int(self.map.turn / 4))
else:
distance = 75
# Scale the emergence value (capped at 10)
armyEmergenceValue = 2 + (armyEmergenceValue * 0.75)
if armyEmergenceValue > 10:
armyEmergenceValue = 10
armyEmergenceScaledToTurns = 5 * armyEmergenceValue / (5 + self.map.turn // 25)
The method then performs a BFS from the emergence point into fog of war:
def foreachFunc(tile, dist):
if not tile.discovered:
distCapped = max(distancePlateau, dist)
emergeValue = distancePlateau * armyEmergenceScaledToTurns // distCapped
# Add to the emergence heat map
self.emergenceLocationMap[emergedTile.player][tile] += max(1, emergeValue)
breadth_first_foreach_dist(self.map, [emergedTile], distance, foreachFunc, bypassDefaultSkip=True)
Key insight: The value added to each fog tile is inversely proportional to distance from the emergence point. Closer tiles get higher scores.
Step 3: The Emergence Heat Map
The emergenceLocationMap is a 2D matrix (one per player) that stores the accumulated prediction score for each tile.
Structure:
self.emergenceLocationMap[player_index] # MapMatrix[float] - scores per tile
How it Builds Up:
- Each emergence event adds value to nearby fog tiles
- Multiple emergences compound - tiles that are near multiple emergence points get higher scores
- Over time, the heat map converges on the most likely general location
Additional Emergence Handling:
The notify_concrete_emergence() method (ArmyTracker.py:1051) handles more certain emergences (when we know the path from fog):
def notify_concrete_emergence(self, maxDist: int, emergingTile: Tile, confidentFromGeneral: bool):
player = emergingTile.player
if confidentFromGeneral:
# Reduce all existing scores (we found the general!)
for tile in self.map.get_all_tiles():
self.emergenceLocationMap[player][tile] /= 5.0
# Add strong evidence around the emergence
incAmount = 500 / len(incTiles) # Higher weight for known paths
for incTile, dist in incTiles:
self.emergenceLocationMap[player][incTile] += incAmount
Step 4: Determining the Predicted Location
The method get_predicted_target_player_general_location() (bot_ek0x45.py:6189) returns the highest-scoring tile.
Algorithm:
def get_predicted_target_player_general_location(self, skipDiscoveredAsNeutralFilter: bool = False) -> Tile:
maxTile = self.general
maxAmount = 0
for tile in self._map.pathable_tiles:
if tile.discovered or tile.isMountain or tile.isNotPathable or tile.isCity:
continue
# Skip tiles that can't be the general
if not self.armyTracker.valid_general_positions_by_player[self.targetPlayer][tile]:
continue
foundValue = 0
# Get the accumulated emergence score
if self.armyTracker.emergenceLocationMap[self.targetPlayer][tile] > 0:
foundValue += self.armyTracker.emergenceLocationMap[self.targetPlayer][tile] * 10
if foundValue > maxAmount:
maxTile = tile
maxAmount = foundValue
return maxTile
The predicted general location is simply the undiscovered fog tile with the highest accumulated emergence score.
Step 5: Constraining Valid General Positions
The valid_general_positions_by_player matrix tracks which tiles could possibly be the general. This gets updated throughout the game.
How tiles become invalid:
- Visible tiles - Once we see a tile, it can’t be the general
- Obstacles - Mountains, swamps, etc. can’t be generals
- Known player tiles - Tiles owned by the player (after capture)
- Distance constraints - Based on launch timing and tile count
The limit_gen_position_from_emergence method (ArmyTracker.py:2787):
This method uses game theory to further constrain where the general can be:
def limit_gen_position_from_emergence(self, p: Player, tile: Tile, emergenceAmount: int = -1):
# Based on when the player launched their first attack, we can limit distance
pLaunchTiming = self.player_launch_timings[tile.player]
maxDist = self.map.turn - pLaunchTiming
maxDist = min(maxDist, p.tileCount - 1)
# Additional constraints based on standing army and timing...
This uses the insight that:
- A player can only move
turn - launch_timingtiles away from their starting position - They can’t have more territory than they’ve had time to capture
Step 6: Fallback Mechanisms
If the emergence-based prediction fails, there are fallbacks:
-
get_max_explorable_undiscovered_tile()
Finds the fog tile furthest from our general that is still reachable (used when enemy general is not yet visible and no emergences have been recorded) -
find_hacky_path_to_find_target_player_spawn_approx()
Uses path-finding to estimate where the enemy general might spawn based on their visible tiles -
Edge case: FFA mode
In free-for-all games with no target player, falls back to a more exploratory strategy
Complete Flow Diagram
Turn Start
│
├─→ ArmyTracker.scan_movement_and_emergences()
│ │
│ └─→ Detects army emerging from fog
│ │
│ ▼
├─→ OpponentTracker.notify_emerged_army()
│ │ Queues emergence
│ ▼
├─→ OpponentTracker._execute_emergence()
│ │ Records emergence stats
│ ▼
└─→ ArmyTracker.new_army_emerged()
│
▼
BFS into fog from emergence
│
▼
Update emergenceLocationMap[player][tile] += value
│
▼
limit_gen_position_from_emergence()
│
▼
Update valid_general_positions
│
▼
┌──────────────────┘
│
▼
bot.get_predicted_target_player_general_location()
│
├─→ For each undiscovered tile:
│ score = emergenceLocationMap[player][tile] * 10
│
└─→ Return tile with highest score
Key Parameters
| Parameter | Value | Description |
min_spawn_distance |
8-10 | Minimum distance from our general |
distance (emergence) |
10-75 | How far into fog to propagate emergence |
armyEmergenceValue |
capped at 10 | Scaled based on army size |
emergence multiplier |
10x | Weight when calculating final prediction |
Why It’s Effective
- Compound evidence: Multiple small emergences build up a clear picture
- Distance weighting: Closer to emergence = higher probability
- Game theory constraints: Uses timing and territory to eliminate impossible positions
- Adaptive: Works early game (few emergences) and late game (many emergences)
The result is “insanely accurate” - even general-hiding strategies typically fail to hide the general location for long, as the bot can infer general location from the trail of armies that must have originated from near it.
Defense System
DangerAnalyzer.py
Identifies threats to the general:
class ThreatObj:
turns: int # Turns until threat arrives
threatValue: int # Army size of threat
path: Path # Path threat will take
threatType: ThreatType # Kill, Vision, etc.
Defense Priority
- Kill threats - Direct attacks on general
- Vision threats - Enemy armies that could reveal general
- City threats - Attacks on friendly cities
Defense Moves
- Block - Move army to block enemy path
- Gather defense - Gather army before enemy arrives
- Interception - Meet enemy army before reaching general
Strategy & Timings
Timings Class
Controls when to switch between gather and expansion phases:
class Timings:
cycleTurns: int # Length of cycle (50 or 100)
splitTurns: int # When to switch gather -> expand
launchTiming: int # When to launch attack
Timing Calculation
Based on:
- Map distance to enemy general
- Tile count - more tiles = longer gather
- Path composition - more enemy/neutral tiles = later launch
- Tile differential - if behind, gather longer
Strategy Modes
All-In
Triggers when significantly behind on economy:
- Abandon defense
- Maximize army gather
- Launch aggressive attack
Flanking
Uses alternate enemy general positions:
- Attack from unexpected direction
- Launch earlier than normal
FFA Mode
Different strategy when multiple players remain:
- Prioritize map edges and city taking
- Don’t prioritize middle until 3 players remain
2v2 Team Play
Communication
- TeammateCommunicator - Sends/receives team messages
- TileCompressor - Compresses tile data for efficient messaging
Coordinated Play
- Share enemy positions
- Coordinate launch timing
- Defend each other’s generals
- Capture cities together
Special Handling
- Track teammate general separately
- Consider teammate tiles in defense planning
- Send launch timing recommendations
Performance Considerations
Time Management
- Turn time limit - 300ms to make move
- Lag detection - Skip expensive calculations if behind
- Per-phase time limits - Expansion, gather each have limits
Key Optimizations
- MapMatrix - Fast O(1) tile access vs dict
- Caching - Distance matrices, path calculations
- Incremental updates - Only recalculate what changed
- Early termination - Cutoff searches that take too long
Performance Timer
Tracks time spent in each phase for debugging and optimization:
with self.perf_timer.begin_move_event('Expansion phase'):
# code here
Key Configuration Parameters
Gather
gather_use_max_set- Use max-set vs max-iterativegather_include_distance_from_enemy_TILES_as_negatives- 3
Expansion
expansion_single_iteration_time_cap- 0.03 secondsexpansion_use_legacy- Use legacy algorithm
Behavior
behavior_launch_timing_offset- 3 (later launch)behavior_flank_launch_timing_offset- −4 (earlier flank)behavior_out_of_play_distance_over_shortest_ratio- 0.45
Engine (MCTS)
engine_use_mcts- Enable Monte Carlo tree searchengine_mcts_scrim_armies_per_player_limit- 2
Known Limitations
- Many bugs - Off-by-one errors, edge cases
- No unit testing - Evolved without proper test infrastructure
- General protection - Doesn’t hide its general well
- Strategic play - No advanced tactics like disguise routes
- Late game - Suboptimal at managing cities vs gather
- 1v1 optimized - Not good at FFA strategy