diff --git a/.tmp_docs/code_wars_documentation.txt b/.tmp_docs/code_wars_documentation.txt new file mode 100644 index 0000000..7b33134 --- /dev/null +++ b/.tmp_docs/code_wars_documentation.txt @@ -0,0 +1,40 @@ + + +--- Page 1 --- + + + +--- Page 2 --- + + + +--- Page 3 --- + + + +--- Page 4 --- + + + +--- Page 5 --- + + + +--- Page 6 --- + + + +--- Page 7 --- + + + +--- Page 8 --- + + + +--- Page 9 --- + + + +--- Page 10 --- + diff --git a/.tmp_docs/codewars.txt b/.tmp_docs/codewars.txt new file mode 100644 index 0000000..bc72f3c --- /dev/null +++ b/.tmp_docs/codewars.txt @@ -0,0 +1,20 @@ + + +--- Page 1 --- + + + +--- Page 2 --- + + + +--- Page 3 --- + + + +--- Page 4 --- + + + +--- Page 5 --- + diff --git a/.tmp_docs/codewars_overview.txt b/.tmp_docs/codewars_overview.txt new file mode 100644 index 0000000..6585d63 --- /dev/null +++ b/.tmp_docs/codewars_overview.txt @@ -0,0 +1,36 @@ + + +--- Page 1 --- + + + +--- Page 2 --- + + + +--- Page 3 --- + + + +--- Page 4 --- + + + +--- Page 5 --- + + + +--- Page 6 --- + + + +--- Page 7 --- + + + +--- Page 8 --- + + + +--- Page 9 --- + diff --git a/.tmp_docs/extract_pdfs.py b/.tmp_docs/extract_pdfs.py new file mode 100644 index 0000000..a47b9a5 --- /dev/null +++ b/.tmp_docs/extract_pdfs.py @@ -0,0 +1,22 @@ +from pathlib import Path +import fitz # PyMuPDF + +FILES = [ + r"c:\Users\OMEN\Downloads\codewars.pdf", + r"c:\Users\OMEN\Downloads\codewars overview.pdf", + r"c:\Users\OMEN\Downloads\code wars documentation.pdf", +] + +OUTDIR = Path(r"c:\Users\OMEN\CodeWarsV6\.tmp_docs") +OUTDIR.mkdir(exist_ok=True) + +for file_path in FILES: + text = [] + doc = fitz.open(file_path) + for i, page in enumerate(doc): + page_text = page.get_text("text") or "" + text.append(f"\n\n--- Page {i + 1} ---\n\n{page_text}") + doc.close() + outpath = OUTDIR / (Path(file_path).stem.replace(" ", "_") + ".txt") + outpath.write_text("".join(text), encoding="utf-8") + print(f"Wrote {outpath}") diff --git a/.tmp_docs/pages/code_wars_documentation_p01.png b/.tmp_docs/pages/code_wars_documentation_p01.png new file mode 100644 index 0000000..cc29ba8 Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p01.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p02.png b/.tmp_docs/pages/code_wars_documentation_p02.png new file mode 100644 index 0000000..39c31d8 Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p02.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p03.png b/.tmp_docs/pages/code_wars_documentation_p03.png new file mode 100644 index 0000000..bf161be Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p03.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p04.png b/.tmp_docs/pages/code_wars_documentation_p04.png new file mode 100644 index 0000000..fca6386 Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p04.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p05.png b/.tmp_docs/pages/code_wars_documentation_p05.png new file mode 100644 index 0000000..595bb10 Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p05.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p06.png b/.tmp_docs/pages/code_wars_documentation_p06.png new file mode 100644 index 0000000..bf1f6dd Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p06.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p07.png b/.tmp_docs/pages/code_wars_documentation_p07.png new file mode 100644 index 0000000..204927d Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p07.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p08.png b/.tmp_docs/pages/code_wars_documentation_p08.png new file mode 100644 index 0000000..c0d5867 Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p08.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p09.png b/.tmp_docs/pages/code_wars_documentation_p09.png new file mode 100644 index 0000000..6507b2a Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p09.png differ diff --git a/.tmp_docs/pages/code_wars_documentation_p10.png b/.tmp_docs/pages/code_wars_documentation_p10.png new file mode 100644 index 0000000..54349d2 Binary files /dev/null and b/.tmp_docs/pages/code_wars_documentation_p10.png differ diff --git a/.tmp_docs/pages/codewars_overview_p01.png b/.tmp_docs/pages/codewars_overview_p01.png new file mode 100644 index 0000000..78748e9 Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p01.png differ diff --git a/.tmp_docs/pages/codewars_overview_p02.png b/.tmp_docs/pages/codewars_overview_p02.png new file mode 100644 index 0000000..4e72a6b Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p02.png differ diff --git a/.tmp_docs/pages/codewars_overview_p03.png b/.tmp_docs/pages/codewars_overview_p03.png new file mode 100644 index 0000000..706a2ef Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p03.png differ diff --git a/.tmp_docs/pages/codewars_overview_p04.png b/.tmp_docs/pages/codewars_overview_p04.png new file mode 100644 index 0000000..becc8a1 Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p04.png differ diff --git a/.tmp_docs/pages/codewars_overview_p05.png b/.tmp_docs/pages/codewars_overview_p05.png new file mode 100644 index 0000000..57aca2d Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p05.png differ diff --git a/.tmp_docs/pages/codewars_overview_p06.png b/.tmp_docs/pages/codewars_overview_p06.png new file mode 100644 index 0000000..0860d0e Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p06.png differ diff --git a/.tmp_docs/pages/codewars_overview_p07.png b/.tmp_docs/pages/codewars_overview_p07.png new file mode 100644 index 0000000..2cf1117 Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p07.png differ diff --git a/.tmp_docs/pages/codewars_overview_p08.png b/.tmp_docs/pages/codewars_overview_p08.png new file mode 100644 index 0000000..745cc45 Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p08.png differ diff --git a/.tmp_docs/pages/codewars_overview_p09.png b/.tmp_docs/pages/codewars_overview_p09.png new file mode 100644 index 0000000..ddf722b Binary files /dev/null and b/.tmp_docs/pages/codewars_overview_p09.png differ diff --git a/.tmp_docs/pages/codewars_p01.png b/.tmp_docs/pages/codewars_p01.png new file mode 100644 index 0000000..0d612ef Binary files /dev/null and b/.tmp_docs/pages/codewars_p01.png differ diff --git a/.tmp_docs/pages/codewars_p02.png b/.tmp_docs/pages/codewars_p02.png new file mode 100644 index 0000000..f024e7c Binary files /dev/null and b/.tmp_docs/pages/codewars_p02.png differ diff --git a/.tmp_docs/pages/codewars_p03.png b/.tmp_docs/pages/codewars_p03.png new file mode 100644 index 0000000..c374473 Binary files /dev/null and b/.tmp_docs/pages/codewars_p03.png differ diff --git a/.tmp_docs/pages/codewars_p04.png b/.tmp_docs/pages/codewars_p04.png new file mode 100644 index 0000000..c89ec77 Binary files /dev/null and b/.tmp_docs/pages/codewars_p04.png differ diff --git a/.tmp_docs/pages/codewars_p05.png b/.tmp_docs/pages/codewars_p05.png new file mode 100644 index 0000000..cf4eedc Binary files /dev/null and b/.tmp_docs/pages/codewars_p05.png differ diff --git a/.tmp_docs/render_pdf_pages.py b/.tmp_docs/render_pdf_pages.py new file mode 100644 index 0000000..fab7589 --- /dev/null +++ b/.tmp_docs/render_pdf_pages.py @@ -0,0 +1,24 @@ +from pathlib import Path +import fitz # PyMuPDF + +FILES = [ + r"c:\Users\OMEN\Downloads\codewars.pdf", + r"c:\Users\OMEN\Downloads\codewars overview.pdf", + r"c:\Users\OMEN\Downloads\code wars documentation.pdf", +] + +OUTDIR = Path(r"c:\Users\OMEN\CodeWarsV6\.tmp_docs\pages") +OUTDIR.mkdir(parents=True, exist_ok=True) + +ZOOM = 2.0 # ~144 DPI for readable text +MATRIX = fitz.Matrix(ZOOM, ZOOM) + +for file_path in FILES: + doc = fitz.open(file_path) + stem = Path(file_path).stem.replace(" ", "_") + for i, page in enumerate(doc, start=1): + pix = page.get_pixmap(matrix=MATRIX, alpha=False) + outpath = OUTDIR / f"{stem}_p{i:02d}.png" + pix.save(outpath) + print(f"Wrote {outpath}") + doc.close() diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b5a294 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda" +} \ No newline at end of file diff --git a/__pycache__/client.cpython-313.pyc b/__pycache__/client.cpython-313.pyc index 0850b97..e7abb8d 100644 Binary files a/__pycache__/client.cpython-313.pyc and b/__pycache__/client.cpython-313.pyc differ diff --git a/about game/important links/documentation.md b/about game/important links/documentation.md new file mode 100644 index 0000000..7b4471c --- /dev/null +++ b/about game/important links/documentation.md @@ -0,0 +1,393 @@ +# Documentation + +[1. Code Structure](https://www.notion.so/1-Code-Structure-def99d4fa18b837bb45d014d145d9356?pvs=21) + +[**2. Core Files Overview**](https://www.notion.so/2-Core-Files-Overview-1ec27b31c11b4087a275e1791b3036b4?pvs=21) + +[3. Scripts](https://www.notion.so/3-Scripts-44f99d4fa18b8335bf5a01b614b38518?pvs=21) + +[](https://www.notion.so/32699d4fa18b80cba30bfc2028cbd7d1?pvs=21) + + + +# Weapon Mechanics + +**Inventory** + +- Maximum weapons per player: `2` +- Default starting weapon: `Desert Eagle (ID 1)` + +**Weapon Spawning** + +- Spawn interval: `15 seconds` +- Pickup radius: `20 units` + +# Weapon Stats + +| ID | Name | Damage | Accuracy | Reload Time | Melee | RPF | Range | ROF | Mag | Ammo | Bullet Speed | Scope | Dual | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | AK47 | 45 | 4 | 2.5 | 30 | 1 | 800 | 0.1 | 35 | 250 | 60 | 4.0 | No | +| 1 | Desert Eagle | 20 | 2 | 1.5 | 15 | 1 | 500 | 0.25 | 15 | 75 | 70 | 2.0 | No | +| 2 | Golden Deagle | 30 | 2 | 1.5 | 25 | 1 | 600 | 0.2 | 15 | 75 | 70 | 4.0 | No | +| 3 | M14 | 100 | 0 | 3.0 | 35 | 1 | 1200 | 0.55 | 6 | 36 | 80 | 4.0 | No | +| 4 | M4 | 40 | 2 | 2.5 | 30 | 1 | 1000 | 0.5 | 24 | 300 | 75 | 5.0 | No | +| 5 | M93BA Sniper | 200 | 0 | 3.5 | 35 | 1 | 1500 | 0.8 | 3 | 20 | 100 | 7.0 | No | +| 6 | Magnum | 30 | 1 | 2.5 | 25 | 1 | 650 | 0.6 | 6 | 36 | 75 | 2.0 | No | +| 7 | MP5 | 25 | 5 | 2.0 | 30 | 1 | 700 | 0.06 | 50 | 400 | 65 | 4.0 | Yes | +| 8 | UZI | 25 | 6 | 1.8 | 20 | 1 | 500 | 0.1 | 40 | 400 | 60 | 4.0 | Yes | +| 9 | TEC9 | 25 | 4 | 1.8 | 25 | 1 | 600 | 0.15 | 40 | 400 | 65 | 4.0 | Yes | +| 10 | SPAS-12 | 75 | 10 | 3.5 | 40 | 5 | 325 | 0.75 | 5 | 24 | 55 | 2.0 | No | +| 11 | SAW | 100 | 0 | 5.0 | 35 | 1 | 1000 | 1.25 | 3 | 6 | 3.0 | 4.0 | No | +| 12 | TAVOR | 9 | 2 | 2.0 | 30 | 1 | 750 | 0.12 | 35 | 200 | 70 | 5.0 | No | +| 13 | XM8 | 8 | 3.25 | 2.2 | 30 | 1 | 875 | 0.085 | 30 | 200 | 72 | 5.0 | No | +| 14 | MINIGUN | 30 | 7 | 4.5 | 35 | 1 | 650 | 0.08 | 50 | 200 | 68 | 4.0 | No | +| 15 | ROCKET LAUNCHER | 100 | 10 | 4.0 | 40 | 1 | 1200 | 0.5 | 3 | 12 | 3.0 | 6.0 | No | + +## Column Definitions + +- **ID:** Unique integer used to identify the weapon in the codebase. +- **Name:** Official display name of the weapon. +- **Damage:** Hit points deducted from a player's 200 HP per bullet hit. +- **Accuracy:** Maximum random angular spread of a fired bullet in degrees. Lower values mean higher precision; 0 is perfectly accurate. +- **Reload Time:** Time in seconds a bot must wait after reloading before it can shoot again. +- **Melee:** Damage dealt when using the weapon in close-range melee. +- **RPF (Rounds per fire)**: The number of projectiles fired each time the weapon shoots once. +- **Range (Effective Range):** Maximum distance at which the weapon remains effective. +- **ROF (Rate of Fire):** Minimum time in seconds between consecutive shots. +- **Mag (Magazine Capacity):** Number of bullets that can be fired before reloading. +- **Ammo Given:** Total reserve ammo granted when the weapon is picked up, excluding the loaded magazine. +- **Bullet Speed:** Pixels a projectile travels per server tick (at 60 ticks/sec), determining how fast it crosses the arena. +- **Scope:** Zoom level or aiming magnification provided by the weapon. +- **Dual:** Whether the weapon can be carried alongside another gun in your two-slot inventory. + +# Grenade Stats + +| ID | Name | Damage | Blast Radius | Fuse Time | Effect Time | Proxy | +| --- | --- | --- | --- | --- | --- | --- | +| 1 | Frag Grenade | 275 | 200 | 3 | 1 | No | +| 2 | Proximity Grenade | 275 | 200 | 5 | 1.5 | Yes | +| 3 | Gas Grenade | 150 | 125 | 2 | 10 | No | + +## Column Definitions + +- **Damage:** Amount of damage dealt when the grenade explodes. +- **Blast Radius:** Distance from the explosion centre where players take damage. +- **Fuse Time:** Time in seconds before the grenade detonates. +- **Effect Duration:** How long the grenade's effect lasts after activation. +- **Proximity Trigger:** If `True`, the grenade explodes automatically when an enemy comes close. + +## 1. Code Structure + +- scripts + - bots + + Contains all the bot scripts + + - core + + bot.py + + game_config.py + + helpers.py + + - map + + Handles map structure, environment data, and collision logic + +- assets + - characters + + Player sprite images and animations + + - grenades + + Visual assets for grenade objects and effects + + - guns + + Gun sprites and weapon-related images + + - medkit + + Images for health pickups + + - sounds + + Sound effects used during gameplay + +- maps + + Stores map related files and layouts used by the game + +- engine + - audio + + Handles audio rendering and sound playback. + + - spawners + + Manages spawn point logic + + - weapons + + Responsible for weapon rendering and visual effects + + +config.py + +main.py + +game.py + +server.py + +client.py + +## **2. Core Files Overview** + +| **File** | **Purpose** | +| --- | --- | +| **config.py** | Central configuration file containing all game settings (physics, weapons, network, map data, spawn points, and game mechanics parameters). | +| **main.py** | Entry point for the application - starts the game server and initializes the game loop (note: not found in current search, likely starts server.py). | +| **game.py** | `PlayerClient` class that manages the local game instance, handles pygame rendering, player input, bot AI integration, and communication with the server via the Network class. | +| **server.py** | `Server` class that runs the game server, manages all player connections, simulates physics/collisions, processes weapon/grenade mechanics, and broadcasts world state to all connected clients. | +| **client.py** | `Network` class that handles socket communication between the client and server, sends player input to the server, and receives world state data (player positions, bullets, grenades, etc.). | + +## 3. Scripts + +- core: + - bot.py + - game_config.py + - helpers.py +- bots: + - player.py + +### 3.1 bot.py + +Loads and executes bot AI scripts in a sandboxed environment, validating that scripts only define a `run()` function (no imports/classes), and injects helper functions to control bot actions while managing bot memory and state. + +--- + +### 3.2 game_config + +Alternative/secondary configuration file containing engine settings (FPS, debug flags, physics parameters, camera controls, player movement speeds, jetpack mechanics, and per-weapon data) separate from the main config.py. + +--- + +### 3.3 player.py + +You are required to implement a single logic function, `run(state, memory)`, inside your team's Python script. This function acts as your bot's sensory input and decision-making centre, and it executes every single frame. + + + +### **Key Points** + +- **Write your logic in the function:** `run(state, memory)` +- Use the `state` object to analyse the game and make decisions +- **NO classes or global variables:** You must use the `memory`string for memory persistence between frames. +- **NO imports:** Do not import `os`, `subprocesses`, or any other libraries. Everything you need is already loaded for you. + +### **What You Need to Do** + +1. **Analyze `state`** + - The `state` object provides real-time telemetry about the environment. + - You can use it to check your coordinates, track enemy positions, detect incoming bullets, and use the array provided to choose your next action. +2. **Execute Actions** + - Based on the data you read, you will call simple action functions to move, aim, shoot, or swap weapons. +3. **Modify `memory` (Optional)** + - Since the function reruns every frame, you are provided with a `memory` string (up to 100 characters) that you can return at the end of the frame. + - It will be passed back to your function on the next frame, allowing you to retain your current strategy, destination, or target. + +Here is a basic structure for your bot: + +```python +def run(state, memory): + # 1. Read the environment + my_x, my_y = state.my_position() + enemies = state.enemy_positions() + + # 2. Make decisions and take actions + if enemies: + # trigger combat actions + pass + + # Example movement condition + if my_y > 0: + # trigger movement/flight actions + pass + + # 3. Return memory for the next frame + return memory +``` + +--- + +### 3.4 helpers + +Provides the bot API with control functions (`move_left()`, `shoot()`, `aim_up()`, etc.), state query methods (`my_position()`, `enemy_positions()`, `my_health()`, etc.), and math utilities for bot scripts to read game state and execute actions. + +### **Built-in Helper Functions** + +### **1. `Movement`** + +Control your bot's position and trajectory. + +- `jetpack()`: Activate jetpack (upward thrust) +- `move_left()`: Move left +- `move_right()`: Move right + +### **2. `Aiming`** + +Adjust your gun's aim direction. + +- `aim_up()`: Aim upward +- `aim_down()`: Aim downward +- `aim_left()`: Aim left +- `aim_right()`: Aim right + +### **3. `Combat`** + +Fire, reload, and switch between weapons. + +- `shoot()`: Fire your current weapon +- `reload()`: Reload your current weapon +- `switch_weapon()`: Switch to your other weapon **** +- `pickup_gun(state)`: Pickup a weapon. The current active weapon is replaced. +- `throw_grenade` : Throws the currently selected grenade. +- `change_grenade_type()` : Cycles to the next grenade type in the order Frag, Proximity, Gas +- `kneel()`: Makes the player kneel. +- `saw_info(state)` : Returns information about visible SAW bullets detected by the player's sensors. + +### Attributes Inside `state` + +--- + +The `state` object passed into your `run(state, memory)` function is your bot's only way to **see the arena**. It's an instance of the `GameState` class and provides a **read-only snapshot** of the world at the current frame. + +Below is the complete reference for every method you can call on `state` to gather intel. + +--- + +### Sensor System + +**Crucial mechanic:** Your bot lacks a global view of the arena. + +You are restricted by a **Sensor Radius**. You can only detect enemies, bullets, grenades, and gun spawns if they are physically within this radius. + +- If you are unarmed, your base sensor radius is **100 pixels**. +- If you are holding a weapon, your sensor radius changes based on the weapon’s scope. + +*Keep this in mind when using the World Info methods below!* + +--- + +### 1. Self/Bot Info + +Use these methods to check your bot's status and resources. + +| Method | Description & Details | +| --- | --- | +| `state.my_position()` | Returns your `(x, y)`pixel coordinates. **Note:** The origin`(0, 0)`is at the top-left of the map. X increases to the right, Y increases downward. | +| `state.my_health()` | Returns your current HP, ranging from`0.0`to`200.0`. If this reaches zero, your bot dies and respawns. | +| `state.my_fuel()` | Returns your jetpack fuel, ranging from`0.0`to`100.0`. Depletes by 0.5 per frame while flying and recharges by 0.5 per frame when grounded. | +| `state.my_score()` | Returns your total kill count for the current match. | +| `state.my_ammo()` | Returns `(current_mag, reserve_ammo)`for your **currently equipped gun only**. If you switch weapons, this updates on the next frame to reflect the new gun. | +| `state.my_aim_angle()` | Returns your current aim angle in radians (`0`=right, `π/2`=down, `-π/2`=up). | +| `state.my_gun()` | Returns the Weapon ID of your currently held gun (or `None` if unarmed). | +| `state._sensor_radius()` | Returns the sensor radius of your current gun, defaults to 30 if you aren’t holding one. | +| `state.get_weapon_stat()` | Returns a specific stat (damage, fire rate, etc.) for a given weapon. | +| `state.my_grenades()` | Returns grenade counts for the player by type | + +### 2. Enemy & Player Info + +Use these to track your targets. + +**Note:** enemy/player methods only return entities currently inside your sensor radius. + +- `state.enemy_positions()` + - **Returns:** A list of `(x, y)` tuples `[(x1, y1), (x2, y2), ...]` +- `state.all_players()` + - **Returns:** A list of dictionaries of type: `{”id”, “x”, “y”, “health”, “score”}` + - **Description:** Provides detailed stats on every living player in your sensor radius, **including yourself**. +- `state.player_markers()` + - **Returns**: A list of dictionaries: `{ "id", "angle", "distance"}` + - **Description**: Returns angle and distance to all active players on the map. No sensor-radius restriction. + +--- + +### 3. Bullet/Grenade Info + +Use these to attempt **evasive manoeuvres**. + +- `state.active_grenades()` + - **Returns:** A list of dictionaries: `{ "x", "y", "vx", "vy", "type" }`, where `type` is the Grenade ID. + - **Description:** Tracks live grenades within your sensor radius. +- `state.bullet_positions()` + - **Returns:** A list of dictionaries of type: `{ "x", "y", "vx", "vy" }` . + - **Description:** Returns the current coordinates and velocities of **all active bullets** flying through your sensor radius, including your own. +- `state.saw_bullets_in_view()` + - **Returns**: A list of dictionaries of type: `{ "x", "y", "vx", "vy", "distance", "owner_id", "slot" }` . + - **Description**: Returns all active visible SAW bullets including velocity and owner information. +- `state.gas_clouds()` + - **Returns**: A list of dictionaries of type: `{ "x", "y", "radius", "duration", "distance"}` + - **Description**: Returns active gas clouds within the bot's sensor radius. + +--- + +### 4. Gun/Medkit spawns + +- `state.gun_spawns()` + - **Returns:** A list of dictionaries: `{ "x", "y", "weapon_id" }` + - **Description:** Shows active pick-up-able weapons on the ground within your sensor radius. +- `state.medkit_spawns()` + - **Returns**: A list of dictionaries: `{ "x", "y"}` + - **Description**: Returns nearby medkit spawn locations within the sensor radius. + +--- + +### 5. Map & Environment (Local Grid + Raycasting) + +- `state.local_map(radius)` + - **Returns:** A 2D list (matrix) of integers. + - **Description:** Returns a square grid of the collision map centred on your bot. Size is `(2*radius + 1) x (2*radius + 1)`. + - **Note:** The maximum allowed radius is the sensor radius and will be used if the radius exceeds it. + - **Values:** `0` = obstacle/wall, `1` = empty passable space. +- `state.distance_to_obstacle(theta, max_distance, step)` + - **Returns:** `float` *(distance in pixels)* + - **Description:** Acts like your bot's **radar**. It tells you how far away the nearest solid wall or map boundary is in a specific direction. + - **Parameters:** + - `theta` → direction angle in **radians** + - `0` = right + - `π/2` = down + - `π` = left + - `3π/2` = up + - `max_distance` *(optional)* → how far the ray travels before stopping *(default 2000)* + - `step` *(optional)* → precision of the calculation in pixels *(default 2.0)* + +--- + +### 6. Match Info + +- `state.time_remaining()` + - **Returns:** The time remaining for the game to end. +- `state.leaderboard()` + - **Returns** : A list of dictionaries: `{ "rank", "id", "kills", "deaths", "kd_delta"}` + - **Description** : Returns the current leaderboard of the match. \ No newline at end of file diff --git a/about game/important links/overview.md b/about game/important links/overview.md new file mode 100644 index 0000000..fa7c7cf --- /dev/null +++ b/about game/important links/overview.md @@ -0,0 +1,273 @@ +# Overview + +[What’s the game about?](https://www.notion.so/What-s-the-game-about-c9b99d4fa18b831c8a0c010e61bb1dac?pvs=21) + +[What are you supposed to do?](https://www.notion.so/What-are-you-supposed-to-do-8e199d4fa18b82518e25015e278970d7?pvs=21) + +# What’s the game about? + +--- + +Welcome to CodeWars V6! This year, we are dropping straight into a fast-paced, 2D multiplayer deathmatch. Inspired by the classic Catacombs map from Mini Militia, your script will control a single, highly mobile combat bot in a free-for-all arena. + +The objective is simple: **accumulate the highest kill score before the match ends.** The battlefield is an 800x500 pixel world filled with solid obstacles, platforms, and open airspace. You'll need to navigate the terrain using your jetpack, scavenge for weapon spawns, hunt down enemy bots, and survive the crossfire. If you die, you will respawn at a random location after a brief delay. + +## **Game Mechanics** + +![image.png](attachment:5afd6928-65b5-4c7f-8679-aeed00a54bdb:image.png) + +### **Your Bot & Resources** + +You are fully responsible for your bot's survival and combat effectiveness. You must manage: + +- **Health:** You spawn with 200 HP, and die when your HP drops to zero. +- **Jetpack Fuel:** You have a capacity of 100 fuel units, allowing you to fly between platforms and gain the high ground. +- **Weapons:** You drop in empty-handed. You must navigate to find weapon spawn points to pick up guns. +- **Loadout:** You can carry and dual-wield up to 2 of the 15 available weapons (snipers, shotguns, assault rifles, etc.), keeping track of your current magazine and reserve ammo. + +Game speed can be changed while testing using arrow keys (Up-increase speed up to 7x or Down-decrease speed) + +### **Health** + +- You start with 200 HP. +- If your bot's **HP drops to 0** from enemy fire, it dies and respawns at a random location after a brief delay. + +### Jetpack + +- Your bot has a jetpack with a maximum capacity of **100 fuel units**, letting you fly between platforms and claim the high ground. +- Fuel depletes at **0.5 units per frame** while thrusting upward and recharges at **0.5 units per frame** when grounded. +- If your fuel hits 0, the jetpack won't activate until it recharges. + +### Weapons + +- You start with both a Primary Weapon and a Secondary Weapon + - **Primary Weapon:** Picked randomly from: (Desert Eagle - 40%, Uzi - 30%, or Tec-9 - 30%). + - **Secondary Weapon:**  a Golden Deagle. +- There are **15** different weapons available (e.g., AK-47, snipers, shotguns, SMGs). +- You can carry and dual-wield up to **2 weapons simultaneously**. Manage your active weapon's magazine and reload using reserve ammo. + +## **How the Game Progresses** + +The game runs at **60 Frames Per Second (FPS)**. Every single frame, your script is evaluated, and you can issue multiple simultaneous commands. You can move left, thrust your jetpack upward, aim your gun, and shoot all at the exact same time. + +## **Game End Conditions** + +Each CodeWars V6 match lasts exactly **6 minutes**. Focus on combat efficiency, movement, and survival. When time runs out, the bot with the **highest kill score** wins. + +# What are you supposed to do? + +--- + +You are required to implement a single logic function, `run(state, memory)`, inside your team's Python script. This function acts as your bot's sensory input and decision-making centre, and it executes every single frame. + +### **Key Points** + +- **Write your logic in the function:** `run(state, memory)` +- Use the `state` object to analyse the game and make decisions +- **NO classes or global variables:** You must use the `memory`string for memory persistence between frames. +- **NO imports:** Do not import `os`, `subprocesses`, or any other libraries. Everything you need is already loaded for you. + +### **What You Need to Do** + +1. **Analyze `state`** + - The `state` object provides real-time telemetry about the environment. + - You can use it to check your coordinates, track enemy positions, detect incoming bullets, and use the array provided to choose your next action. +2. **Execute Actions** + - Based on the data you read, you will call simple action functions to move, aim, shoot, or swap weapons. +3. **Modify `memory` (Optional)** + - Since the function reruns every frame, you are provided with a `memory` string (up to 100 characters) that you can return at the end of the frame. + - It will be passed back to your function on the next frame, allowing you to retain your current strategy, destination, or target. + + +Here is a basic structure for your bot: + +```python +def run(state, memory): + # 1. Read the environment + my_x, my_y = state.my_position() + enemies = state.enemy_positions() + + # 2. Make decisions and take actions + if enemies: + # trigger combat actions + pass + + # Example movement condition + if my_y > 0: + # trigger movement/flight actions + pass + + # 3. Return memory for the next frame + return memory +``` + +# **Built-in Helper Functions** + +### **1. `Movement`** + +Control your bot's position and trajectory. + +- `jetpack()`: Activate jetpack (upward thrust) +- `move_left()`: Move left +- `move_right()`: Move right + +### **2. `Aiming`** + +Adjust your gun's aim direction. + +- `aim_up()`: Aim upward +- `aim_down()`: Aim downward +- `aim_left()`: Aim left +- `aim_right()`: Aim right + +### **3. `Combat`** + +Fire, reload, and switch between weapons. + +- `shoot()`: Fire your current weapon +- `reload()`: Reload your current weapon +- `switch_weapon()`: Switch to your other weapon **** +- `pickup_gun(state)`: Pickup a weapon. The current active weapon is replaced. +- `throw_grenade` : Throws the currently selected grenade. +- `change_grenade_type()` : Cycles to the next grenade type in the order Frag, Proximity, Gas +- `kneel()`: Makes the player kneel. +- `saw_info(state)` : Returns information about visible SAW bullets detected by the player's sensors. + +# Attributes Inside `state` + +--- + +The `state` object passed into your `run(state, memory)` function is your bot's only way to **see the arena**. It's an instance of the `GameState` class and provides a **read-only snapshot** of the world at the current frame. + +Below is the complete reference for every method you can call on `state` to gather intel. + +--- + +### Sensor System + +**Crucial mechanic:** Your bot lacks a global view of the arena. + +You are restricted by a **Sensor Radius**. You can only detect enemies, bullets, grenades, and gun spawns if they are physically within this radius. + +- If you are unarmed, your base sensor radius is **100** **pixels**. +- If you are holding a weapon, your sensor radius changes based on the weapon’s scope. + +*Keep this in mind when using the World Info methods below!* + +--- + +### 1. Self/Bot Info + +Use these methods to check your bot's status and resources. + +| Method | Description & Details | +| --- | --- | +| `state.my_position()` | Returns your `(x, y)`pixel coordinates. **Note:** The origin`(0, 0)`is at the top-left of the map. X increases to the right, Y increases downward. | +| `state.my_health()` | Returns your current HP, ranging from`0.0`to`200.0`. If this reaches zero, your bot dies and respawns. | +| `state.my_fuel()` | Returns your jetpack fuel, ranging from`0.0`to`100.0`. Depletes by 0.5 per frame while flying and recharges by 0.5 per frame when grounded. | +| `state.my_score()` | Returns your total kill count for the current match. | +| `state.my_ammo()` | Returns `(current_mag, reserve_ammo)`for your **currently equipped gun only**. If you switch weapons, this updates on the next frame to reflect the new gun. | +| `state.my_aim_angle()` | Returns your current aim angle in radians (`0`=right, `π/2`=down, `-π/2`=up). | +| `state.my_gun()` | Returns the Weapon ID of your currently held gun (or `None` if unarmed). | +| `state._sensor_radius()` | Returns the sensor radius of your current gun, defaults to 30 if you aren’t holding one. | +| `state.get_weapon_stat()` | Returns a specific stat (damage, fire rate, etc.) for a given weapon. | +| `state.my_grenades()` | Returns grenade counts for the player by type | + +### 2. Enemy & Player Info + +Use these to track your targets. + +**Note:** enemy/player methods only return entities currently inside your sensor radius. + +- `state.enemy_positions()` + - **Returns:** A list of `(x, y)` tuples `[(x1, y1), (x2, y2), ...]` +- `state.all_players()` + - **Returns:** A list of dictionaries of type: `{”id”, “x”, “y”, “health”, “score”}` + - **Description:** Provides detailed stats on every living player in your sensor radius, **including yourself**. +- `state.player_markers()` + - **Returns**: A list of dictionaries: `{ "id", "angle", "distance"}` + - **Description**: Returns angle and distance to all active players on the map. No sensor-radius restriction. + +--- + +### 3. Bullet/Grenade Info + +Use this to attempt **evasive manoeuvres**. + +- `state.active_grenades()` + - **Returns:** A list of dictionaries: `{ "x", "y", "vx", "vy", "type" }`, where `type` is the Grenade ID. + - **Description:** Tracks live grenades within your sensor radius. +- `state.bullet_positions()` + - **Returns:** A list of dictionaries of type: `{ "x", "y", "vx", "vy" }` . + - **Description:** Returns the current coordinates and velocities of **all active bullets** flying through your sensor radius, including your own. +- `state.saw_bullets_in_view()` + - **Returns**: A list of dictionaries of type: `{ "x", "y", "vx", "vy", "distance", "owner_id", "slot" }` . + - **Description**: Returns all active visible SAW bullets including velocity and owner information. +- `state.gas_clouds()` + - **Returns**: A list of dictionaries of type: `{ "x", "y", "radius", "duration", "distance"}` + - **Description**: Returns active gas clouds within the bot's sensor radius. + +--- + +### 4. Gun/Medkit spawns + +- `state.gun_spawns()` + - **Returns:** A list of dictionaries: `{ "x", "y", "weapon_id" }` + - **Description:** Shows active pick-up-able weapons on the ground within your sensor radius. +- `state.medkit_spawns()` + - **Returns**: A list of dictionaries: `{ "x", "y"}` + - **Description**: Returns nearby medkit spawn locations within the sensor radius. + +--- + +### 5. Map & Environment (Local Grid + Raycasting) + +- `state.local_map(radius)` + - **Returns:** A 2D list (matrix) of integers. + - **Description:** Returns a square grid of the collision map centred on your bot. Size is `(2*radius + 1) x (2*radius + 1)`. + - **Note:** The maximum allowed radius is the sensor radius and will be used if the radius exceeds it. + - **Values:** `0` = obstacle/wall, `1` = empty passable space. +- `state.distance_to_obstacle(theta, max_distance, step)` + - **Returns:** `float` *(distance in pixels)* + - **Description:** Acts like your bot's **radar**. It tells you how far away the nearest solid wall or map boundary is in a specific direction. + - **Parameters:** + - `theta` → direction angle in **radians** + - `0` = right + - `π/2` = down + - `π` = left + - `3π/2` = up + - `max_distance` *(optional)* → how far the ray travels before stopping *(default 2000)* + - `step` *(optional)* → precision of the calculation in pixels *(default 2.0)* + +--- + +### 6. Match Info + +- `state.time_remaining()`: + - **Returns:** The time remaining for the game to end. +- `state.leaderboard()`: + - **Returns** : A list of dictionaries: `{ "rank", "id", "kills", "deaths", "kd_delta"}` + - **Description** : Returns the current leaderboard of the match. + +--- + +### **Common Mistakes That Will Fail Validation** + +| Mistake | Reason | +| --- | --- | +| Adding any `import` or `from … import` statement | All imports are **forbidden**. | +| Missing **`run`** function | The script **must** contain a function named `run`. | +| Having functions other than `run` | The script can contain only **one** function, ie. `run`. | +| Using **global variables** to **persist state** | There are **no persistent globals** between frames. Use the `memory` string parameter instead. Any global state is reset each time the module is re-executed. | +| Letting `memory` exceed **100 characters** | Anything beyond that is truncated. Structure your memory string accordingly. | + +--- + +- If the team **meets all conditions**, it **passes validation**. Otherwise, it fails and loses the match. + +## **Goal** + +- Write an intelligent bot script that maximises kills and minimises deaths through strategic movement, aiming, and weapon management. +- **Optimise** your strategy to counter enemy movements effectively. + +You can use the provided **helper functions** or create your own logic to optimise your strategy! \ No newline at end of file diff --git a/about game/main_pg.md b/about game/main_pg.md new file mode 100644 index 0000000..796eb95 --- /dev/null +++ b/about game/main_pg.md @@ -0,0 +1,95 @@ +# CodeWars V6: Mini Militia! + +*The sixth edition of our flagship algo bot programming contest, brought to you by the Web and Coding Club, IIT Bombay!* + +Download the Game Engine from here: [github.com/wncc/CodeWarsV6](https://github.com/wncc/CodeWarsV6). + +Join the CodeWars V6 WhatsApp group using [this link](https://chat.whatsapp.com/DnACQnVVXKn2pnCsbUmQPw) or scan the QR code below: + +![image.png](attachment:7835f606-80c4-42fd-9275-526af919086f:image.png) + +**If you encounter an error that you believe is caused by the Game Engine rather than your code, please re-download the Game Engine from our GitHub page. If that doesn't resolve the issue, let us know.** + +## Important Rules and Disqualification Details + +### What you are allowed to do: + +1. You can change `player.py` file. Change the name of your scripts and add other scripts using: [](https://www.notion.so/32699d4fa18b80cba30bfc2028cbd7d1?pvs=21) +2. You can only implement one function, run(state, memory) +3. You can use the helper functions detailed [here](https://www.notion.so/Overview-b5799d4fa18b82e5884a016f1c2138a6?pvs=21) to implement your strategies. +4. Collaborating with other teams is allowed. There will be no plagiarism check either, but it's up to you whether you want to help your opponent! + +### What you are NOT allowed to do: + +1. You are not allowed to edit any files outside the scripts/bots folder or config.py. Do NOT modify the game engine files by yourself. +2. You are not allowed to use any game engine components that are not documented. +3. **Global variables** are thus **NOT ALLOWED*** (other than `memory`). +4. You are not allowed to import any libraries. The libraries `numpy`, `random`, `math` are already imported for you. +5. You are not allowed to rename the run function or add any other extra functions in your script file. + + + +** If you feel your use of global variables does not keep state, please consult your mentor* + + + +# Important Links + +[What’s the game about?](https://www.notion.so/What-s-the-game-about-c9b99d4fa18b831c8a0c010e61bb1dac?pvs=21) + +[What are you supposed to do?](https://www.notion.so/What-are-you-supposed-to-do-8e199d4fa18b82518e25015e278970d7?pvs=21) + +[**Built-in Helper Functions**](https://www.notion.so/Built-in-Helper-Functions-3979eafd9c414e299d37a21f3b6c435e?pvs=21) + +[Attributes Inside `state`](https://www.notion.so/Attributes-Inside-state-7d8e8b730875415db5ca6758060ce612?pvs=21) + +[](https://www.notion.so/32699d4fa18b80cba30bfc2028cbd7d1?pvs=21) + +# Contest Guidelines + +[Timeline ](https://www.notion.so/Timeline-31e99d4fa18b80bb8f3ee4b144c2ee39?pvs=21) + +[Rules and Guidelines ](https://www.notion.so/Rules-and-Guidelines-31e99d4fa18b80828289d553bd8f6f93?pvs=21) + +[Mentor Guidelines ](https://www.notion.so/Mentor-Guidelines-31e99d4fa18b802f9c40ff2368e7f081?pvs=21) + +[Selection Round](https://www.notion.so/Selection-Round-93c99d4fa18b8395af730156a4b34257?pvs=21) + +# The Game + +[Installation](https://www.notion.so/Installation-20099d4fa18b82799515819218843798?pvs=21) + +[Setup & Run](https://www.notion.so/Setup-Run-31e99d4fa18b80a08a21eddb0a1a66fe?pvs=21) + +[Overview](https://www.notion.so/Overview-b5799d4fa18b82e5884a016f1c2138a6?pvs=21) + +[Documentation](https://www.notion.so/Documentation-ad899d4fa18b827f86f401afc4133700?pvs=21) + +[Sample Scripts](https://www.notion.so/Sample-Scripts-31e99d4fa18b80a7b970ef0e2334a602?pvs=21) + +The slides used in the introductory session on 11th March, 2026 are available here: + +https://www.canva.com/design/DAHDen7WV94/9HUzhU3WVgBdjE4qGLWGGg/edit + +# **Resources** + +[GIT](https://www.notion.so/GIT-73899d4fa18b82a3b1bf81830a9a0b2c?pvs=21) + +[Python](https://www.notion.so/Python-41f99d4fa18b82ecaa08816a198af4c3?pvs=21) + +## FAQs and Common Technical Errors + +If you have any queries, please check the FAQs to see if they have already been answered. + +If not, please contact your mentor/super mentor, or Utkarsh <8826799261> and Abhishek <6361016050> via WhatsApp. + +*We’ll keep updating this section as we receive queries from teams.* + +### FAQs + +What to do if my game suddenly stops/hangs? + +Restart the server and then run the game again. The game should work properly now. \ No newline at end of file diff --git a/about game/rules.md b/about game/rules.md new file mode 100644 index 0000000..d4800f9 --- /dev/null +++ b/about game/rules.md @@ -0,0 +1,33 @@ +# Rules and Guidelines + +- Teams can have up to 4 members. +- You can use the functions and variables we provide, but attempting to access any private variables or functions of the game not provided by us will result in the disqualification of your team. +- **Only one Global variable** is **ALLOWED** (`memory`). If you feel your use of global variables does not maintain state, consult your mentor to see if it is allowed. +- You cannot use any of the following libraries/modules: `os` ,`subprocess` and other libraries that can run commands. +- You can change `a.py` and `b.py` files. +- Collaborating with other teams is allowed. There will be no plagiarism check either, but it's up to you whether you want to help your opponent! +- Ensure `memory` does not exceed the allowed length of 100. If `memory` exceeds this limit at any point during the match, that team will **instantly lose**. +- If the team **meets all conditions**, it **passes validation**. Otherwise, it fails and loses the match. + +- Each team must submit only one script by __ March, 2026. No submissions or modifications will be accepted after this deadline. + - Note: The deadline will be updated soon. Stay tuned! + +## Game End Conditions + +- Each match runs for a fixed duration with 6 teams competing. +- When the timer ends, the **team with the most kills wins**. +- If two or more teams have the **same number of kills**, tie-breaker conditions apply: + - The team with **fewer deaths** wins. + - If both kills and deaths are equal, the match ends with multiple winners. + + +### **🔴 Common Mistakes That Will Fail Validation** + +| Mistake | Reason | +| --- | --- | +| Missing **`team_name`**, **`troops`**, **`deploy_list`**, or **`team_signal`** | All four variables **must** exist. | +| `troops` has **less/more than 8** elements | `troops` **must** contain **exactly** 8 troop types. | +| `troops` has **duplicate troop types** | The **8 troops must be unique**. | +| `team_signal` is **longer than `SIGNAL_LENGTH`** | The signal must be **shorter than the limit**. | +| `Deploy` or `Utils` **is missing** | These classes must be **present in the script**. | +| Class `My` is created by me and used in the script. | Any other class other than Deploy or Utils is not allowed. | \ No newline at end of file diff --git a/client.py b/client.py index 27b64f9..c04861b 100644 --- a/client.py +++ b/client.py @@ -1,17 +1,44 @@ import socket +import time import numpy as np import config class Network: def __init__(self): self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.timeout = 5.0 + self.client.settimeout(self.timeout) # default to localhost for local testing self.host = config.SERVER_HOST self.port = config.SERVER_PORT self.addr = (self.host, self.port) def connect(self, name): - self.client.connect(self.addr) + last_err = None + for attempt in range(1, 6): + try: + self.client.connect(self.addr) + break + except (socket.timeout, ConnectionRefusedError, OSError) as e: + last_err = e + if attempt < 5: + time.sleep(0.5) + continue + else: + if self.host not in ("127.0.0.1", "localhost"): + # Retry once against localhost for local dev setups. + self.client.close() + self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client.settimeout(self.timeout) + fallback_addr = ("127.0.0.1", self.port) + try: + self.client.connect(fallback_addr) + self.host, self.addr = fallback_addr[0], fallback_addr + print(f"[CLIENT] Falling back to {self.host}:{self.port} after connect failure: {last_err}") + except (socket.timeout, ConnectionRefusedError, OSError): + raise + else: + raise # inform server of username - pad to exactly 16 bytes name_bytes = name.encode('utf-8')[:16] # Truncate if too long diff --git a/config.py b/config.py index 412887b..2de4f1b 100644 --- a/config.py +++ b/config.py @@ -7,7 +7,10 @@ # NETWORK SETTINGS # ============================================================================= SERVER_PORT = 5555 -SERVER_HOST = "192.168.56.1" # Change to your server IP +# Host the client connects to. +SERVER_HOST = "127.0.0.1" # Localhost by default; change to your server IP for LAN play +# Host the server binds to. +SERVER_BIND_HOST = "0.0.0.0" # Bind all interfaces so local clients can connect reliably # ============================================================================= # MAP SETTINGS @@ -20,7 +23,7 @@ # ============================================================================= # MATCH SETTINGS # ============================================================================= -MATCH_DURATION = 180.0 # seconds +MATCH_DURATION = 360.0 # seconds (6 minutes) # ============================================================================= # PLAYER/TANK SETTINGS @@ -51,11 +54,11 @@ # List of bot script filenames (without .py) BOT_SCRIPTS = [ - "random_bot", + "player", "random_bot", "random_bot", "pro_bot", - "debug_bot" + "debug_bot", ] # If no keyboard player, first script bot renders @@ -88,6 +91,19 @@ MAX_BULLET_DISTANCE = 1200 # Max travel distance before deactivation BULLET_VISUAL_RADIUS = 100 +# ============================================================================= +# SENSOR SETTINGS (BOT VISION) +# ============================================================================= +SENSOR_RADIUS_UNARMED = 30 +SENSOR_RADIUS_DEFAULT = 30 +SENSOR_RADIUS_MIN = 10 +SENSOR_RADIUS_MAX = 50 +# Overrides to match documented examples. +SENSOR_RADIUS_OVERRIDES = { + 2: 10, # Golden Deagle + 5: 50, # M93BA Sniper +} + # ============================================================================= # GUN WEAPON STATS # Structured dictionary for clarity and future expansion @@ -96,7 +112,7 @@ WEAPON_STATS = { 0: { "name": "AK47", - "damage": 45, + "damage": 10, "accuracy": 4, "reload_time": 2.5, "melee": 30, @@ -112,7 +128,7 @@ }, 1: { "name": "Desert Eagle", - "damage": 20, + "damage": 8, "accuracy": 2, "reload_time": 1.5, "melee": 15, @@ -128,7 +144,7 @@ }, 2: { "name": "Golden Deagle", - "damage": 30, + "damage": 10, "accuracy": 2, "reload_time": 1.5, "melee": 25, @@ -144,7 +160,7 @@ }, 3: { "name": "M14", - "damage": 100, + "damage": 36, "accuracy": 0, "reload_time": 3, "melee": 35, @@ -160,7 +176,7 @@ }, 4: { "name": "M4", - "damage": 40, + "damage": 14, "accuracy": 2, "reload_time": 2.5, "melee": 30, @@ -176,7 +192,7 @@ }, 5: { "name": "M93BA Sniper", - "damage": 200, + "damage": 75, "accuracy": 0, "reload_time": 3.5, "melee": 35, @@ -208,7 +224,7 @@ }, 7: { "name": "MP5", - "damage": 25, + "damage": 7, "accuracy": 5, "reload_time": 2, "melee": 30, @@ -224,7 +240,7 @@ }, 8: { "name": "UZI", - "damage": 25, + "damage": 7, "accuracy": 6, "reload_time": 1.8, "melee": 20, @@ -240,7 +256,7 @@ }, 9: { "name": "TEC9", - "damage": 25, + "damage": 10, "accuracy": 4, "reload_time": 1.8, "melee": 25, @@ -256,7 +272,7 @@ }, 10: { "name": "SPAS-12", - "damage": 75, + "damage": 25, "accuracy": 10, "reload_time": 3.5, "melee": 40, @@ -354,7 +370,7 @@ # ============================================================================= # GUN SPAWN SYSTEM # ============================================================================= -GUN_SPAWN_INTERVAL = 30 # Seconds before gun respawns +GUN_SPAWN_INTERVAL = 15 # Seconds before gun respawns GUN_PICKUP_RADIUS = 20.0 # Distance to pick up gun MAX_GUNS_PER_PLAYER = 2 # Inventory size DEFAULT_STARTING_WEAPON = 1 # Desert Eagle @@ -434,7 +450,7 @@ def get_random_starting_weapon(): # ============================================================================= # RENDERING SETTINGS # ============================================================================= -GAME_FPS = 30 # Client FPS +GAME_FPS = 60 # Client FPS SERVER_FPS = 60 # Server tick rate # Background Colors @@ -525,6 +541,21 @@ def get_weapon_stat(weapon_id, stat_name): return WEAPON_STATS[weapon_id][stat_name] +def get_sensor_radius(weapon_id): + """Get bot sensor radius in pixels for a given weapon ID.""" + if weapon_id is None: + return SENSOR_RADIUS_UNARMED + + if weapon_id in SENSOR_RADIUS_OVERRIDES: + return SENSOR_RADIUS_OVERRIDES[weapon_id] + + scope = get_weapon_stat(weapon_id, "scope") + if scope is None: + return SENSOR_RADIUS_DEFAULT + + radius = int(round(scope * 10)) + return max(SENSOR_RADIUS_MIN, min(SENSOR_RADIUS_MAX, radius)) + def get_all_weapon_ids(): """Get list of all weapon IDs""" return list(WEAPON_STATS.keys()) diff --git a/engine/weapons/weapons.py b/engine/weapons/weapons.py index 0ca0ba9..2fe2e5a 100644 --- a/engine/weapons/weapons.py +++ b/engine/weapons/weapons.py @@ -115,9 +115,9 @@ def __init__(self, grenade_id, name, damage, blast_radius, fuse_time, sprite_fil self.is_proxy = proxy grenades = { - 1: Grenade(1, "Frag Grenade", damage=275, blast_radius=200, fuse_time=3, effect_time=1, sprite_file="frag_grenade.png", proxy=False), - 2: Grenade(2, "Proximity Grenade", damage=275, blast_radius=200, fuse_time=5, effect_time=1.5, sprite_file="prox_grenade.png", proxy=True), - 3: Grenade(3, "Gas Grenade", damage=150, blast_radius=125, fuse_time=2, effect_time=10, sprite_file="gas_grenade.png", proxy=False) + 1: Grenade(1, "Frag Grenade", damage=1000, blast_radius=100, fuse_time=3, effect_time=1, sprite_file="frag_grenade.png", proxy=False), + 2: Grenade(2, "Proximity Grenade", damage=800, blast_radius=80, fuse_time=5, effect_time=1.5, sprite_file="prox_grenade.png", proxy=True), + 3: Grenade(3, "Gas Grenade", damage=750, blast_radius=100, fuse_time=2, effect_time=10, sprite_file="gas_grenade.png", proxy=False) } def get_grenade(grenade_id): diff --git a/game.py b/game.py index c8cff0e..b2a04e2 100644 --- a/game.py +++ b/game.py @@ -739,7 +739,7 @@ def quit_game(self): def launch_bot(script_name): PlayerClient(script_name=script_name, render=False) -if __name__ == "__main__": +def main(): pygame.init() threads = [] @@ -757,3 +757,7 @@ def launch_bot(script_name): PlayerClient(script_name=None, render=True) else: PlayerClient(script_name=config.BOT_SCRIPTS[0], render=True) + + +if __name__ == "__main__": + main() diff --git a/main.py b/main.py new file mode 100644 index 0000000..90992d7 --- /dev/null +++ b/main.py @@ -0,0 +1,14 @@ +import threading + +from server import Server +import game + + +def _run_server(): + Server() + + +if __name__ == "__main__": + server_thread = threading.Thread(target=_run_server, daemon=True) + server_thread.start() + game.main() diff --git a/scripts/bots/player.py b/scripts/bots/player.py new file mode 100644 index 0000000..608e6aa --- /dev/null +++ b/scripts/bots/player.py @@ -0,0 +1,473 @@ +def run(state, memory): + # Persistent state. 'recovery' = frames to commit to roam_dir after unstick. + roam_dir = 1 + patrol_idx = 0 + jump_cd = 0 + grenade_cd = 0 + stuck = 0 + last_x = 0 + last_y = 0 + climb_ticks = 0 + fly_ticks = 0 + recovery = 0 + + if memory: + try: + p = memory.split(",") + if len(p) >= 9: + roam_dir = -1 if int(p[0]) < 0 else 1 + patrol_idx = int(p[1]) + jump_cd = max(0, int(p[2])) + grenade_cd = max(0, int(p[3])) + stuck = max(0, int(p[4])) + last_x = int(p[5]) + last_y = int(p[6]) + climb_ticks = max(0, int(p[7])) + fly_ticks = max(0, int(p[8])) + if len(p) >= 10: + recovery = max(0, int(p[9])) + except Exception: + pass + + x, y = state.my_position() + health = state.my_health() + fuel = state.my_fuel() + aim = state.my_aim_angle() + ammo_cur, ammo_total = state.my_ammo() + current_gun = state.my_gun() + enemies = state.enemy_info() + markers = state.player_markers() + grenades = state.my_grenades() + + if jump_cd > 0: jump_cd -= 1 + if grenade_cd > 0: grenade_cd -= 1 + if climb_ticks > 0: climb_ticks -= 1 + if fly_ticks > 0: fly_ticks -= 1 + if recovery > 0: recovery -= 1 + + cur_dmg = 0 + cur_range = 0 + if current_gun is not None: + cur_dmg = state.get_weapon_stat(current_gun, "damage") or 0 + cur_range = state.get_weapon_stat(current_gun, "effective_range") or 0 + + patrol_points = [ + (38, 105), (200, 150), (350, 50), (500, 100), (750, 150), + (887, 291), (926, 656), (1141, 677), (1281, 291), (1546, 998), + (1575, 990), (1811, 286), (2017, 709), (2163, 299), (2378, 617), (2698, 1056), + ] + patrol_len = len(patrol_points) + patrol_idx = patrol_idx % patrol_len + + best_gun = None + best_gun_dist = 999999.0 + for gun in state.gun_spawns(): + wid = gun["weapon_id"] + dmg = state.get_weapon_stat(wid, "damage") or 0 + rng = state.get_weapon_stat(wid, "effective_range") or 0 + gdx = gun["x"] - x + gdy = gun["y"] - y + gdist = math.sqrt(gdx * gdx + gdy * gdy) + need = current_gun is None or cur_dmg < 12 + if need or dmg >= cur_dmg + 4 or rng >= cur_range + 120: + if gdist < best_gun_dist: + best_gun_dist = gdist + best_gun = gun + + nearest_medkit = None + nearest_medkit_dist = 999999.0 + for mk in state.medkit_spawns(): + mdx = mk["x"] - x + mdy = mk["y"] - y + mdist = math.sqrt(mdx * mdx + mdy * mdy) + if mdist < nearest_medkit_dist: + nearest_medkit_dist = mdist + nearest_medkit = mk + + danger_g = None + danger_dist = 999999.0 + for g in state.active_grenades(): + gdx = x - g["x"] + gdy = y - g["y"] + gdist = math.sqrt(gdx * gdx + gdy * gdy) + if gdist < danger_dist: + danger_dist = gdist + danger_g = g + + in_gas = False + gas_flee_dir = 1 + for cloud in state.gas_clouds(): + if cloud["distance"] < cloud["radius"] + 20.0: + in_gas = True + gas_flee_dir = -1 if x > cloud["x"] else 1 + break + + # ========================================================= + # PRIORITY 1: Escape live grenades. + # Threshold raised to 220 = frag blast radius so the bot starts + # running before the explosion reaches it. + # ========================================================= + if danger_g is not None and danger_dist < 220.0: + if x < danger_g["x"]: + move_left() + roam_dir = -1 + else: + move_right() + roam_dir = 1 + if fuel > 40.0 and jump_cd <= 0: + jetpack() + jump_cd = 20 + if ammo_cur <= 0 and ammo_total > 0: + reload() + + # ========================================================= + # PRIORITY 2: Gas cloud escape + # ========================================================= + elif in_gas: + if gas_flee_dir < 0: + move_left() + else: + move_right() + if fuel > 20.0: + jetpack() + + # ========================================================= + # PRIORITY 3: Critical health — heal before doing anything else + # if a medkit is reachable (< 200 px). Moved above combat/markers + # so the bot doesn't get finished off while chasing enemies. + # ========================================================= + elif health < 65.0 and nearest_medkit is not None and nearest_medkit_dist < 200.0: + tx = nearest_medkit["x"] + ty = nearest_medkit["y"] + move_dir = -1 if tx < x else 1 + roam_dir = move_dir + if move_dir < 0: + move_left() + else: + move_right() + front_angle = math.pi if move_dir < 0 else 0.0 + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=2.0) + mk_above = ty < y - 60.0 + if front_dist < 18.0: + climb_ticks = 12 + if mk_above: + climb_ticks = 12 + if mk_above and fuel > 18.0: + jetpack() + elif fuel > 32.0 and climb_ticks > 0: + jetpack() + if nearest_medkit_dist < 22.0: + pickup() + + # ========================================================= + # PRIORITY 4: Combat — visible enemies in sensor range + # ========================================================= + elif enemies: + target = min(enemies, key=lambda e: e["distance"]) + ex = float(target["x"]) + ey = float(target["y"]) + dist = float(target["distance"]) + dx = ex - x + dy = ey - y + angle = math.atan2(dy, dx) + + err = angle - aim + if err > math.pi: err -= 2.0 * math.pi + elif err < -math.pi: err += 2.0 * math.pi + if err > 0.02: aim_right() + elif err < -0.02: aim_left() + + # FIX: use step=2.0 so thin walls are not skipped by raycasting. + obs_dist = state.distance_to_obstacle(angle, max_distance=2000.0, step=2.0) + blocked = obs_dist < max(0.0, dist - 16.0) + + # Enemy is below a floor: dy > 40 means enemy is significantly lower. + # Moving toward them would mean trying to walk through solid ground. + enemy_below_floor = blocked and dy > 40.0 + + # Enemy is above a ceiling: dy < -40 means enemy is significantly higher + # and a solid surface blocks the path — don't jetpack into ceiling. + enemy_above_ceiling = blocked and dy < -40.0 + + # Enemy separated by a solid wall/floor/ceiling — no direct path. + # The bot should roam horizontally instead of charging or jumping. + enemy_behind_solid = enemy_below_floor or enemy_above_ceiling + + move_dir = -1 if dx < 0 else 1 + roam_dir = move_dir + front_angle = math.pi if move_dir < 0 else 0.0 + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=2.0) + + if enemy_behind_solid: + # Move horizontally to find a gap, ledge, or stairs. + # Reverse if a wall blocks the horizontal path. + if front_dist < 18.0: + if move_dir < 0: move_right() + else: move_left() + else: + if move_dir < 0: move_left() + else: move_right() + # No jetpack — need to navigate around, not fly into solid surfaces. + elif dist > 190.0 or blocked: + if move_dir < 0: move_left() + else: move_right() + # Only climb/jetpack when NOT blocked by a solid surface between + # us and the enemy. If blocked, the obstacle is between us — flying + # up won't help and wastes fuel / looks like hallucination. + if not blocked: + if front_dist < 18.0: climb_ticks = 10 + if dy < -60.0 and abs(dx) < 150.0: climb_ticks = 10 + if fuel > 28.0 and climb_ticks > 0: jetpack() + if fuel > 46.0 and jump_cd <= 0 and front_dist < 18.0: + jetpack() + jump_cd = 16 + climb_ticks = 10 + else: + # Blocked but not below/above — side wall. Only climb to + # get over the wall itself, don't aggressively chase. + if front_dist < 18.0 and fuel > 38.0 and jump_cd <= 0: + jetpack() + jump_cd = 18 + climb_ticks = 8 + elif dist < 80.0: + if move_dir < 0: move_right() + else: move_left() + else: + if move_dir < 0: move_left() + else: move_right() + if front_dist < 18.0: climb_ticks = 8 + if dy < -60.0 and abs(dx) < 150.0: climb_ticks = 8 + if fuel > 28.0 and climb_ticks > 0: jetpack() + + # FIX: check BOTH the enemy angle AND the actual current aim direction + # before firing. The bullet travels along `aim`, not `angle`, so if + # aim is slightly off and points into a wall the bullet hits the wall. + aim_obs = state.distance_to_obstacle(aim, max_distance=2000.0, step=2.0) + aim_clear = aim_obs >= dist - 16.0 + if not blocked and aim_clear and abs(err) < 0.12: + if ammo_cur > 0: + shoot() + elif ammo_total > 0: + reload() + else: + switch_weapon() + elif ammo_cur <= 0 and ammo_total > 0: + reload() + + # ------------------------------------------------------- + # Grenade strategy — safe distances only. + # + # Blast radii (pixels): frag=200, proxy=200, gas=125 + # Safe throw rule: bot must be OUTSIDE the blast when it + # explodes, i.e. dist > blast_radius + 20 px margin. + # + # Gas (blast 125): safe when dist > 145 + # Frag (blast 200): safe when dist > 220 + # Proxy (blast 200): safe when dist > 220 + # + # Also check the throw path is not blocked by a wall + # (otherwise grenade bounces back). + # ------------------------------------------------------- + if not blocked and not enemy_behind_solid and grenade_cd <= 0: + total_nades = grenades["frag"] + grenades["proxy"] + grenades["gas"] + # Verify no wall stands between us and the throw target. + throw_wall = state.distance_to_obstacle(angle, max_distance=300.0, step=2.0) + path_ok = throw_wall >= dist * 0.85 + if total_nades > 0 and path_ok: + desired = 0 + if 145.0 < dist < 220.0 and grenades["gas"] > 0: + desired = 3 # gas: safe at this range + elif dist > 220.0 and grenades["frag"] > 0: + desired = 1 # frag: outside its own blast radius + elif dist > 220.0 and grenades["proxy"] > 0: + desired = 2 # proxy: outside blast, enemy walks into it + if desired > 0: + if grenades["selected_type"] != desired: + change_grenade_type() + else: + throw_grenade() + grenade_cd = 65 + + if health < 80.0 and nearest_medkit is not None and nearest_medkit_dist < 25.0: + pickup() + + # ========================================================= + # PRIORITY 5: Long-range pursuit via global player markers + # ========================================================= + elif markers: + target = min(markers, key=lambda m: m["distance"]) + angle = float(target["angle"]) + dist = float(target["distance"]) + + err = angle - aim + if err > math.pi: err -= 2.0 * math.pi + elif err < -math.pi: err += 2.0 * math.pi + if err > 0.02: aim_right() + elif err < -0.02: aim_left() + + # FIX: the key detour fix. + # When recovery > 0 the bot was just stuck. The anti-stuck already + # flipped roam_dir to the opposite direction. Use that flipped + # roam_dir for horizontal movement instead of blindly following the + # marker angle — this makes the bot go AROUND the obstacle rather + # than walking straight back into the same wall. + # When recovery == 0 operate normally and update roam_dir. + if recovery > 0: + move_dir = roam_dir + # Also sustain upward flight during recovery to help clear walls. + if fuel > 18.0: + jetpack() + else: + move_dir = -1 if math.cos(angle) < 0 else 1 + roam_dir = move_dir + + if move_dir < 0: + move_left() + else: + move_right() + + front_angle = math.pi if move_dir < 0 else 0.0 + front_dist = state.distance_to_obstacle(front_angle, max_distance=96.0, step=2.0) + + target_above = -2.8 < angle < -0.35 + very_far = dist > 350.0 + + if front_dist < 28.0: + climb_ticks = 14 + if target_above: + fly_ticks = 14 + if very_far: + fly_ticks = 16 + + # Sustained upward flight when target is above (no jump_cd gate). + if target_above and fuel > 18.0: + jetpack() + elif fuel > 28.0 and (climb_ticks > 0 or fly_ticks > 0): + jetpack() + + if fuel > 36.0 and jump_cd <= 0 and (front_dist < 28.0 or very_far): + jetpack() + jump_cd = 12 + if very_far: fly_ticks = 14 + else: climb_ticks = 14 + + # Opportunistic long-range shots using step=2.0 for accuracy. + obs = state.distance_to_obstacle(angle, max_distance=2000.0, step=2.0) + if obs >= dist - 16.0 and abs(err) < 0.12: + if ammo_cur > 0: + shoot() + elif ammo_total > 0: + reload() + + # ========================================================= + # PRIORITY 6: Medkit recovery when health is low + # ========================================================= + elif health < 120.0 and nearest_medkit is not None: + tx = nearest_medkit["x"] + ty = nearest_medkit["y"] + move_dir = -1 if tx < x else 1 + roam_dir = move_dir + if move_dir < 0: move_left() + else: move_right() + + front_angle = math.pi if move_dir < 0 else 0.0 + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=2.0) + mk_above = ty < y - 60.0 + if front_dist < 18.0: climb_ticks = 12 + if mk_above: climb_ticks = 12 + if mk_above and fuel > 18.0: jetpack() + elif fuel > 32.0 and climb_ticks > 0: jetpack() + if not mk_above and fuel > 46.0 and jump_cd <= 0 and front_dist < 18.0: + jetpack() + jump_cd = 18 + climb_ticks = 12 + if nearest_medkit_dist < 22.0: + pickup() + + # ========================================================= + # PRIORITY 7: Weapon upgrade + # ========================================================= + elif best_gun is not None: + tx = best_gun["x"] + ty = best_gun["y"] + move_dir = -1 if tx < x else 1 + roam_dir = move_dir + if move_dir < 0: move_left() + else: move_right() + + front_angle = math.pi if move_dir < 0 else 0.0 + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=2.0) + gun_above = ty < y - 60.0 + if front_dist < 18.0: climb_ticks = 12 + if gun_above: climb_ticks = 12 + if gun_above and fuel > 18.0: jetpack() + elif fuel > 32.0 and climb_ticks > 0: jetpack() + if not gun_above and fuel > 46.0 and jump_cd <= 0 and front_dist < 18.0: + jetpack() + jump_cd = 18 + climb_ticks = 12 + if best_gun_dist < 22.0: + pickup_gun(state) + + # ========================================================= + # PRIORITY 8: Patrol + # ========================================================= + else: + tx, ty = patrol_points[patrol_idx] + if abs(tx - x) < 38.0: + patrol_idx = (patrol_idx + 1) % patrol_len + tx, ty = patrol_points[patrol_idx] + + move_dir = -1 if tx < x else 1 + roam_dir = move_dir + front_angle = math.pi if move_dir < 0 else 0.0 + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=2.0) + + if move_dir < 0: move_left() + else: move_right() + + if front_dist < 18.0: climb_ticks = 12 + if ty < y - 80.0 and abs(tx - x) < 120.0: climb_ticks = 12 + + if fuel > 28.0 and (climb_ticks > 0 or fly_ticks > 0): jetpack() + if fuel > 42.0 and jump_cd <= 0 and (front_dist < 18.0 or (ty < y - 80.0 and abs(tx - x) < 120.0)): + jetpack() + jump_cd = 20 + if ty < y - 80.0: fly_ticks = 10 + else: climb_ticks = 12 + + roam_target = math.pi if move_dir < 0 else 0.0 + roam_err = roam_target - aim + if roam_err > math.pi: roam_err -= 2.0 * math.pi + elif roam_err < -math.pi: roam_err += 2.0 * math.pi + if roam_err > 0.08: aim_right() + elif roam_err < -0.08: aim_left() + + # Global opportunistic weapon pickup — every frame regardless of priority. + if best_gun is not None and best_gun_dist < 22.0: + pickup_gun(state) + + # ========================================================= + # Anti-stuck + # During recovery the stuck counter is frozen so the bot + # can't immediately re-trigger another detour event. + # ========================================================= + moved = math.sqrt((x - last_x) ** 2 + (y - last_y) ** 2) + if recovery == 0: + if moved < 2.0: + stuck += 1 + else: + stuck = 0 + + if stuck > 25: + roam_dir = -roam_dir # flip — markers branch will use this during recovery + climb_ticks = 16 + fly_ticks = 12 + if fuel > 40.0 and jump_cd <= 0: + jetpack() + jump_cd = 20 + recovery = 55 + stuck = 0 + + memory = f"{roam_dir},{patrol_idx},{jump_cd},{grenade_cd},{stuck},{int(x)},{int(y)},{climb_ticks},{fly_ticks},{recovery}" + return memory[:100] diff --git a/scripts/bots/pro_bot.py b/scripts/bots/pro_bot.py index b86cad5..704a25e 100644 --- a/scripts/bots/pro_bot.py +++ b/scripts/bots/pro_bot.py @@ -14,7 +14,8 @@ # state.my_gun() -> int or None (current weapon id) # state.my_ammo() -> (current, total) # state.my_grenades() -> {selected_type, frag, proxy, gas} -# state.enemy_positions() -> [{id,x,y,health,current_gun,secondary_gun,current_grenade,distance}] +# state.enemy_positions() -> [(x, y), ...] +# state.enemy_info() -> [{id,x,y,health,current_gun,secondary_gun,current_grenade,distance}] # state.gun_spawns() -> [{x,y,weapon_id}] # state.medkit_spawns() -> [{x,y}] # state.active_grenades() -> [{x,y,vx,vy,type}] @@ -49,8 +50,8 @@ def run(state, memory): "fly_tick": 0, } - # Use enemy_positions for actual coordinates anywhere on map (not quadrant-limited). - enemies = state.enemy_positions() + # Use enemy_info for detailed coordinates and stats within sensor radius. + enemies = state.enemy_info() markers = state.player_markers() ammo_cur, _ = state.my_ammo() grenades = state.my_grenades() @@ -172,6 +173,17 @@ def run(state, memory): force_close = memory["no_damage_ticks"] >= 16 + # Basic obstacle-aware movement toward/away from target. + desired_dir = -1 if dx < 0 else 1 + front_angle = math.pi if desired_dir == -1 else 0.0 + back_angle = 0.0 if desired_dir == -1 else math.pi + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=4.0) + back_dist = state.distance_to_obstacle(back_angle, max_distance=64.0, step=4.0) + front_blocked = front_dist < 20.0 + + if front_blocked and fuel > 6.0: + jetpack() + # Movement based on actual pixel x-position, not angle hemisphere. if escaping_nade: if memory["nade_escape_dir"] < 0: @@ -194,18 +206,24 @@ def run(state, memory): jetpack() elif distance > MAX_FIGHT_DIST: # Close the gap: move directly toward enemy. - if dx < 0: + if front_blocked and back_dist > front_dist: + move_right() if desired_dir == -1 else move_left() + elif dx < 0: move_left() else: move_right() elif distance < MIN_FIGHT_DIST and (not force_close): # Back away. - if dx < 0: + if front_blocked and back_dist > front_dist: + move_left() if desired_dir == -1 else move_right() + elif dx < 0: move_right() else: move_left() elif force_close and distance > PRESSURE_DIST: - if dx < 0: + if front_blocked and back_dist > front_dist: + move_right() if desired_dir == -1 else move_left() + elif dx < 0: move_left() else: move_right() @@ -289,7 +307,18 @@ def run(state, memory): elif m_error < -0.01: aim_left() - if m_angle > 1.57 or m_angle < -1.57: + desired_dir = -1 if math.cos(m_angle) < 0 else 1 + front_angle = math.pi if desired_dir == -1 else 0.0 + back_angle = 0.0 if desired_dir == -1 else math.pi + front_dist = state.distance_to_obstacle(front_angle, max_distance=64.0, step=4.0) + back_dist = state.distance_to_obstacle(back_angle, max_distance=64.0, step=4.0) + front_blocked = front_dist < 20.0 + if front_blocked and fuel > 6.0: + jetpack() + + if front_blocked and back_dist > front_dist: + move_right() if desired_dir == -1 else move_left() + elif desired_dir == -1: move_left() else: move_right() diff --git a/scripts/bots/simple_bot.py b/scripts/bots/simple_bot.py index 03a85bd..1205e3a 100644 --- a/scripts/bots/simple_bot.py +++ b/scripts/bots/simple_bot.py @@ -6,11 +6,7 @@ def run(state, memory): enemies = state.enemy_positions() if enemies: - target = enemies[0] - if isinstance(target, dict): - ex, ey = target["x"], target["y"] - else: - ex, ey = target[0], target[1] + ex, ey = enemies[0] if ex < x: move_left() diff --git a/scripts/core/helpers.py b/scripts/core/helpers.py index 5648442..1050a90 100644 --- a/scripts/core/helpers.py +++ b/scripts/core/helpers.py @@ -5,7 +5,6 @@ import time import math import config -from config import BULLET_VISUAL_RADIUS from config import WEAPON_STATS as WEAPONS # ========================= @@ -157,19 +156,13 @@ def my_aim_angle(self): return float(self.__world[self.__id, 3]) def _sensor_radius(self): - gun = self.my_gun() + return float(config.get_sensor_radius(self.my_gun())) - if gun is None: - return BULLET_VISUAL_RADIUS + def sensor_radius(self): + """Return the bot's current sensor radius in pixels.""" + return self._sensor_radius() - scope = config.get_weapon_stat(gun, "scope") - - if scope is None: - return BULLET_VISUAL_RADIUS - - return scope * BULLET_VISUAL_RADIUS - - def enemy_positions(self): + def _enemy_info(self): radius = self._sensor_radius() px, py = self.my_position() @@ -212,6 +205,14 @@ def enemy_positions(self): return enemy_data + def enemy_positions(self): + """Returns a list of (x, y) tuples for enemies within sensor radius.""" + return [(e["x"], e["y"]) for e in self._enemy_info()] + + def enemy_info(self): + """Returns detailed enemy info for bots that need it.""" + return self._enemy_info() + def all_players(self): radius = self._sensor_radius() @@ -236,6 +237,7 @@ def all_players(self): "x": ex, "y": ey, "health": float(self.__world[i, 7]), + "score": float(self.__world[i, 8]), }) return players @@ -287,15 +289,21 @@ def local_map(self, radius): return result - def get_weapon_stat(self, weapon_name, stat): + def get_weapon_stat(self, weapon_id, stat): """ Returns a specific stat of a weapon. Example: - get_weapon_stat("sniper", "damage") + get_weapon_stat(5, "damage") """ - weapon = WEAPONS.get(weapon_name) + weapon = WEAPONS.get(weapon_id) + if weapon is None and isinstance(weapon_id, str): + # Best-effort name lookup for older scripts. + for wid, wstats in WEAPONS.items(): + if str(wstats.get("name", "")).lower() == weapon_id.lower(): + weapon = wstats + break if weapon is None: return None diff --git a/server.py b/server.py index a12c1c6..e44de7c 100644 --- a/server.py +++ b/server.py @@ -88,8 +88,10 @@ def _start_server(self, PORT): self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - HOST_NAME = socket.gethostname() - SERVER_IP = socket.gethostbyname(HOST_NAME) + SERVER_IP = getattr(config, "SERVER_BIND_HOST", None) + if not SERVER_IP: + HOST_NAME = socket.gethostname() + SERVER_IP = socket.gethostbyname(HOST_NAME) try: self.server_socket.bind((SERVER_IP, PORT)) @@ -100,7 +102,7 @@ def _start_server(self, PORT): self.server_socket.listen() - print(f"[SERVER] Server Started with local ip {SERVER_IP}") + print(f"[SERVER] Server Started with bind ip {SERVER_IP}") return True def setup_game(self): @@ -1661,5 +1663,6 @@ def player_handler(self, conn, player_id): -a = Server() -print("program concluded") \ No newline at end of file +if __name__ == "__main__": + Server() + print("program concluded")