A Python library for creating AI bots for AIA's game collection. This library allows you to programmatically create node-based AI logic that can be exported and used in Unity games.
If you are an LLM writing a bot from this README, jump to Notes for LLM Authors at the bottom first. It explains the mental model, lists the mistakes we keep seeing, and shows a correct minimal script.
This library requires Python 3.7+. Place the AIGamePyLibrary package folder in your project directory and import it:
from AIGamePyLibrary import *Aialander PyLib is the friendly name; the importable Python package matches the GitHub repo: AIGamePyLibrary (one name, no separate “Library” / “PyLibrary” split).
Here's a simple example for Volleyball. Everything you read here — positions, velocities, "the ball", "the opponent" — is a node you ask the graph for via a simulation-prefixed helper (VolleyballGetVector3, VolleyballGetTransform, VolleyballGetBool, VolleyballGetFloat). These match the VolleyballGet* / SlimeGetVector3 node types in Assets/_Nodes/ (the Vector3 node type id is still SlimeGetVector3 in Unity; the Python API is VolleyballGetVector3 so naming stays consistent). There are no Unity-style dotted accessors like something.Position / something.Velocity — always go through the helpers.
from AIGamePyLibrary import *
# Initialize the slime with name, color, country, and stats (speed, acceleration, jump)
InitializeSlime("AIA", "Yellow", "United States of America", 5, 3, 2)
# Grab the world Vector3s from the graph. Every call returns a Node.
ball_position = VolleyballGetVector3("Ball Position")
self_position = VolleyballGetVector3("Self Position")
# Team spawn is a Transform; convert to a Vector3 via RelativePosition.
team_spawn = VolleyballGetTransform("Self Team Spawn")
positionSign = RelativePosition(team_spawn, "Backward")
# Calculate where to move (ball position + offset)
moveTo = ball_position + positionSign * 0.4
# Calculate distance to ball and jump condition
distanceToBall = Distance(ball_position, self_position)
jumpCondition = distanceToBall < 2.25
# Control the slime (target position, jump condition)
SlimeController(moveTo, jumpCondition)
# Save the AI data to a file
SaveData("SlimeVolleyball/AIComp_Data/Saves/AIA python.txt", "grid")The library uses a node-based system where operations return Node objects. Nodes can be combined using Python operators:
- Arithmetic:
+,-,*,/,//,%,** - Comparison:
<,<=,>,>=,==,!= - Boolean:
&(and),|(or),^(xor),~(not)
Example:
ball_pos = VolleyballGetVector3("Ball Position")
self_pos = VolleyballGetVector3("Self Position")
distance = Distance(ball_pos, self_pos)
shouldJump = distance < 2.5 # Returns a Node representing the comparisonVector3 nodes support component access and operations. Always get the Vector3 from a simulation-prefixed helper (VolleyballGetVector3(...) in Volleyball, RelativePosition(transform_node, "Self") for any Transform node in other sims) — Transforms do not have a .Position / .position attribute.
# Access vector components on a Vector3 Node
ballPos = VolleyballGetVector3("Ball Position")
x = ballPos.x # X component
y = ballPos.y # Y component
z = ballPos.z # Z component
# Vector arithmetic between Vector3 Nodes
offset = VolleyballGetVector3("Ball Position") - VolleyballGetVector3("Self Position")
scaled = offset * 0.5Node configurations determine which nodes are available in the Unity editor. Each configuration has a specific purpose. Expand the lists below to see nodes by configuration.
Default Nodes
Description: General purpose nodes for all simulations. Includes arithmetic, vectors, logic, variables, and debug tools.
Basic Types
| Node | Purpose | Inputs | Outputs | Options |
|---|---|---|---|---|
Float(value) |
Represents a real number | — |
|
— |
Bool(value) |
Represents a true/false or “boolean” value | — |
|
— |
String(value) |
Represents a text value | — |
|
— |
Color(value) |
Outputs the color value selected in the dropdown | — |
|
See Color options |
Country(value) |
Outputs the country value selected in the dropdown | — |
|
See Country options |
Color options
"Auburn", "Black", "Blonde", "Blue", "Brown", "Dark Brown", "Dark Green", "Green", "Hot Pink", "Light Blue", "Light Grey", "Medium Grey", "Orange", "Pink", "Purple", "Red", "Tan", "White", "Yellow"
Country options
"Unknown", "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Bermuda", "Bohemia", "Botswana", "Brazil", "Bulgaria", "Burkina Faso", "Burundi", "Cameroon", "Canada", "Chile", "China", "Colombia", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia", "Côte d'Ivoire", "Denmark", "Djibouti", "Dominican Republic", "DR Congo", "Ecuador", "Egypt", "Eritrea", "Estonia", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guyana", "Haiti", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kosovo", "Kuwait", "Kyrgyzstan", "Latvia", "Lebanon", "Lithuania", "Luxembourg", "Malaysia", "Mauritius", "Mexico", "Moldova", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Netherlands", "New Zealand", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palestine", "Panama", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Puerto Rico", "Qatar", "Romania", "Russia", "Samoa", "San Marino", "Saudi Arabia", "Scotland", "Senegal", "Serbia", "Singapore", "Slovakia", "Slovenia", "Somolia", "South Africa", "South Korea", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States of America", "Uruguay", "Uzbekistan", "Venezuela", "Vietnam", "Virgin Islands", "Yemen", "Zambia", "Zimbabwe", "ChatGPT", "Claude", "Deepseek", "Gemini", "Grok", "Llama", "Mistral", "Perplexity", "Qwen"
Arithmetic Operations
| Node | Alias | Purpose | Inputs | Outputs |
|---|---|---|---|---|
AddFloats(a, b) |
a + b |
Performs an addition operation between two numbers |
|
|
SubtractFloats(a, b) |
a - b |
Performs a subtract operation between two numbers |
|
|
MultiplyFloats(a, b) |
a * b |
Performs a multiplication operation between two numbers |
|
|
DivideFloats(a, b) |
a / b |
Performs a divide operation between two numbers |
|
|
Modulo(a, b) |
a % b |
Divides a number by another number and returns any remainder |
|
|
ClampFloat(value, min, max) |
— | Limits (clamps) a number between a minimum and maximum value |
|
|
RandomFloat(min, max) |
— | Returns a random number value between two values (changes every Update) |
|
Math Functions
| Node | Alias | Purpose | Inputs | Outputs | Options |
|---|---|---|---|---|---|
AbsFloat(x) |
Abs(x) |
Convert a number to it’s unsigned / absolute value |
|
|
— |
Operation(x) |
— | Performs the selected operation on the input number |
|
|
Operations |
Power(base, exponent) |
a ** b |
Raises base to the given power (Mathf.Pow) |
|
— | |
Lerp(a, b, t) |
— | Linearly interpolates between A and B by T (Mathf.Lerp) |
|
— |
Operation options
abs, round, floor, ceil, sin, cos, tan, asin, acos, atan, sqrt, sign, ln, log10, e^, 10^
Vector Operations
| Node | Alias | Purpose | Inputs | Outputs |
|---|---|---|---|---|
AddVector3(a, b) |
a + b |
Adds two three-dimensional vectors to each other |
|
|
SubtractVector3(a, b) |
a - b |
Subtracts the second Vector3 from the first (component-wise) |
|
|
ScaleVector3(vec, scalar) |
vec * scalar |
Multiplies a Vector3 by a number |
|
|
DotProduct(a, b) |
a @ b |
Returns the dot product between two vectors |
|
|
CrossProduct(a, b) |
— | Calculates the cross product (result perpendicular to both inputs) |
|
|
Magnitude(vec) |
— | Returns the length of the input vector |
|
|
Normalize(vec) |
— | Returns a vector with the same direction but magnitude 1 |
|
|
Distance(pos1, pos2) |
— | Calculates the distance between two points |
|
|
Vector3Split(vec) |
— | Splits a Vector3 into x, y, z components |
|
|
ConstructVector3(x, y, z) |
Vector3(x, y, z) |
Creates a Vector3 from three numbers |
|
Comparison & Logic Operations
| Node | Alias | Purpose | Inputs | Outputs | Options |
|---|---|---|---|---|---|
CompareFloats(a, b, operator) |
a < b, a > b, … |
Evaluates two float values against the selected operator |
|
Operators | |
CompareBool(a, b, operator) |
— | Evaluates two boolean values against the selected operator |
|
Operators | |
Not(condition) |
~condition |
Toggles the input boolean (TRUE↔FALSE) |
|
|
— |
ConditionalSetFloat(condition, trueValue, falseValue) |
— | Selects between two Float values based on a condition |
|
Dropdown | |
ConditionalSetVector3(condition, trueValue, falseValue) |
— | Selects between two Vector3 values based on a condition |
|
Dropdown | |
ConditionalSetBool(condition, trueValue, falseValue) |
— | Selects between two Bool values based on a condition |
|
Dropdown |
CompareFloats operators
==, <, >, <=, >=
CompareBool operators
AND, OR, EQUAL TO, XOR, NOR, NAND, XNOR
ConditionalSetFloat dropdown
True (use trueValue when condition is true), False (use trueValue when condition is false)
ConditionalSetBool / ConditionalSetVector3 dropdown
True, False
Variables & Utilities
| Node | Purpose | Inputs | Outputs | Options |
|---|---|---|---|---|
SetVariable(value) |
Saves the input value so it can be used by matching GetVariable nodes |
|
— | Destination node |
GetVariable(name) |
Outputs the value from the corresponding SetVariable node with the same typed name |
— |
|
— |
Relay(value) |
Passes through data from input to output (useful for organization) |
|
|
— |
IsNull(value) |
Checks if the input is null |
|
|
— |
Keypress(key) |
Indicates whether the selected key is currently pressed | — |
|
Key is selected in dropdown |
RelativePosition(transform, direction) |
Gets a world-space position relative to the input Transform and selected direction |
|
|
Self, Self + Forward, Self + Backward, Self + Left, Self + Right, Self + Up, Self + Down, Forward, Backward, Left, Right, Up, Down, World |
RelativePosition options
Self, Self + Forward, Self + Backward, Self + Left, Self + Right, Self + Up, Self + Down, Forward, Backward, Left, Right, Up, Down, World
Note: World exists in the Unity dropdown; currently it behaves the same as Self unless/until the Unity gate assigns it a distinct meaning.
Debug & Visualization
| Node | Purpose | Inputs | Outputs |
|---|---|---|---|
Debug(value) |
Displays the real-time value of the connected output |
|
— |
DebugDrawLine(start, end, width, color) |
Draws a 2D line in worldspace (debug visualization) | — | |
DebugDrawDisc(center, radius, height, color) |
Draws a 2D disc in worldspace on the XY plane (debug visualization) | — | |
TimePlot(name, color, iconUrl, value) |
Adds a value to the time plot graph during a simulation (toggle with F1) | — |
Organization
| Node | Purpose | Inputs | Outputs |
|---|---|---|---|
Region |
Groups nodes visually for organization (does not affect logic) | — | — |
CreateFunction(name) |
Defines a named custom function body (Unity: Construct Custom Function). Nodes assigned to its bounds run only when a matching CustomFunction call evaluates them — not during the global graph solve. |
|
|
SetFunctionReturn(fn, body_output) |
Required for a call result. Connects a body output (e.g. Power.Float1) to CreateFunction Return port Any1 (polarity In). |
body_output — any body node with an out port |
— |
AssignToFunction(body_node, fn) |
Marks a node as owned by the CreateFunction body (ownerFunctionSID) |
— | Returns the same body node |
CustomFunction(name, param1=None, …) |
Call site for a CreateFunction by name (Unity key Function). Passes through used params; output is the definition’s Return value when SetFunctionReturn was used. |
|
|
Custom Functions (how they differ from normal nodes)
- Definition vs call:
CreateFunction("MyFn")defines a reusable body;CustomFunction("MyFn", …)calls it by name (modifier). - Parameters: Up to 4 Param outputs on the definition (
Any1–Any4→fn.Param1…Param4). Wire them into body nodes. Only params that are connected on the definition appear as inputs on call sites. - Return (required for a usable call result): Call
SetFunctionReturn(fn, body_output). This connects the body node's output port to the CreateFunction Return input.- Return port id is
Any1, polarity In (GameObject name"Any - In"). Param1 is alsoAny1but polarity Out — same id, different polarity. - For
Power/Lerp/ most float math, the body output port isFloat1→ wire to ReturnAny1(In). - Without
SetFunctionReturn,CustomFunction(...)still runs the body but the call output is null.
- Return port id is
- Body bounds: Mark every body node with
AssignToFunction(body_node, fn)(ownerFunctionSID). Body nodes are skipped by the global solve and only run when aFunctioncall evaluates them. - Isolation: Body cannot wire to the outside world except via Param / Return. Banned in body: nested
CreateFunction/Function,SetVariable, and most destination nodes (controllers / properties). Allowed destinations in body:Debug,DebugDrawLine,DebugDrawDisc. Max nested call depth: 8. - Unlike
Region: Region is visual-only. CreateFunction bounds change execution. - Unlike Python helpers in
customNodes.py: those expand into ordinary nodes at compile time. Unity Custom Functions are theCreateFunction/Functionpair. Native math nodes likePower(...)/Lerp(...)are normal graph nodes and are ideal inside a function body.
from AIGamePyLibrary import *
fn = CreateFunction("PowerFn")
# Body: params → Power → Return
# fn.Param1 (Any1) = base
# fn.Param2 (Any2) = exponent
# Power.Float1 = result → MUST go to CreateFunction Return (Any1 In)
powered = AssignToFunction(Power(fn.Param1, fn.Param2), fn)
SetFunctionReturn(fn, powered) # Power.Float1 → Return Any1 (In) ← do not omit
# Call with different variables; CustomFunction output IS the Return value
result = CustomFunction("PowerFn", Float(2), Float(3)) # → 8
Debug(result, "2^3", changePosition=False)
SaveData("PowerFunctionTest.txt", "grid")Full three-call test: Examples/CustomFunctionPowerExample.py.
Survival Simulation
Overview
Aialanders are trapped on a deserted island with apple trees and other inhabitants. Each Aialander has a storage container they can put food. Tree count and location can vary per simulation. The number of competitors and their strategies will vary per simulation. The maximum number of total players is 7.
Objective: Survive on the island given the available food. Other inhabitants may be friendly or aggressive — choose any personality you want.
Requirements
- The
SurvivalController(...)node is the required destination to control a single 3D character. ConstructSurvivalProperties(...)sets that character’s aesthetic.
Nodes
| Node | Purpose | Inputs | Outputs | Options / Notes |
|---|---|---|---|---|
SurvivalController(targetPosition, state, sprint, emote) |
Controls an Aialander’s brain (navigation uses pathfinding) |
|
— | Destination node |
ConstructSurvivalProperties(...) |
Sets cosmetic options for this Aialander |
|
— | Customization options are documented under Details → Customization. |
SurvivalGetTransform(value) |
Select a Transform representing a current location / entity in the sim | — |
|
Options |
SurvivalGetFloat(value) |
Select a Float representing a current simulation parameter | — |
|
Options |
SurvivalGetBool(value) |
Select a Bool representing a current simulation parameter | — |
|
Options |
SurvivalState(value) |
Select a SurvivalState value | — |
|
Passive, Gather, Eat, Attack, Steal, Dead |
SurvivalEmote(value) |
Select an emote | — |
|
None, Hi, Talk, Bored, Wave |
SurvivalAutoPosition(state) |
Automatically decide where to move based on a given state |
|
|
— |
ConditionalSetSurvivalState(condition, trueValue, falseValue) |
Select between two SurvivalState values based on a condition |
|
|
True, False |
ConditionalSetSurvivalEmote(condition, trueValue, falseValue) |
Select between two SurvivalEmote values based on a condition |
|
|
True, False |
Details
Body Type
0Pants and shorts1Dress2Robot
Beard Styles
0None1Full trimmed beard
Hair Styles
0Curtains1Caesar2Bob3Faux hawk4Long polytail5Pebbles6Side pony7Samurai side bun8Undercut9Medium polytail10Bowl11Space buns12Samurai bun13Bald
Custom Outfit The properties node has a custom outfit at the bottom that can take a URL string to an image. You can upload the image to something like imgur and then copy the image URL.
If you want, here's the PSD to make a custom outfit texture for your character:
https://github.com/theaia/AIGamePyLibrary/blob/main/CustomOutfit.psd
Health
- Each player starts with a designated container with 250 health.
- Containers do not regenerate health.
- When a container's health reaches 0 it will be destroyed along with any food stored in it.
Terrain
- Terrain has an approximate diameter of 160.
- For the competition the seed will be random—you may be the closest one to a tree, or the furthest.
- I'll be running the sim with both abundant and scarce resources. Adjust your strategy appropriately.
- After being harvested, fruit respawn after 190 seconds. Each tree starts with and has a maximum of 3 fruit.
Players are sorted by Survival Time, then Most Health, then Hunger, then stored apple count. When a player dies, their survival time stops at the time they die.
Abilities
- Standard move speed is 5.
- Sprinting increases player speed 9. Players must have available stamina to sprint.
- Attacking deals 10 damage per hit and consumes 5 stamina. Players must have enough stamina to attack. Attack range is 2. Attack radius is 1 in the forward direction of the player. Attacking while holding food will drop the food.
- Players will drop food they are holding when they die.
- Emoting will stop all movement and cause the player to perform a looping animation until their emote state is set to "None".
Health
- Players all start with 100 health.
- Players will regenerate 1 health per Tick after they've not taken damage for 5 seconds.
- After a player has started regenerating health, their "last attacked by" player is reset.
Hunger
- Players start with 100 Hunger points.
- Every 5 seconds players lose 10 points. The 5 second timer is reset whenever a player eats.
- If the player does not have 10 hunger to be consumed, they will instead take 10 damage.
- Consuming food restores 25 points.
Stamina
- Players all start with 100 stamina.
- Sprinting consumes stamina at .15 per tick and Attacking consumes stamina at 5 per attack.
- After not consuming stamina for 3 seconds players will regenerate 2 stamina per Tick.
Aggression
- Players have an aggression level.
- Attacking a player with the same or lower aggression level will increase a player's aggression by 1.
- Attacking a player's container with the same or lower aggression level will increase a player's aggression by .5.
- Players can reference players based on aggression using the Get Transform and Get Float nodes.
- Stealing from an alive player's container will increase aggression by .25.
- Stealing from a dead player's inventory does not affect a player's aggression level.
- Passive - The player will not perform any actions during this state.
- Gathering - If not carrying any food a player will search for nearby food to gather and automatically pick it from trees or off the ground. When carrying food the player will automatically deposit it into its own storage container if nearby.
- Eating - If not carrying any food a player will search for nearby food to gather and automatically pick it from trees, off of the ground, or from its own container. When carrying food the player will automatically consume it.
- Attack - If not carrying any food a player will search for nearby players and containers and will automatically attack them when in range. When carrying food the player will automatically drop it.
- Steal - If not carrying any food a player will search for nearby containers they do not own to steal from. When carrying food the player will automatically deposit it into its own storage container if nearby.
- Dead - The player will not perform any actions during this state.
0Self1Player Last Damaged By2Player Last Attacking (Global)3Player Nearest4Player 2nd Nearest5Player Farthest6Player with Highest Health7Player with Lowest Health8Player with Highest Hunger9Player with Lowest Hunger10Player with Highest Stamina11Player with Lowest Stamina12Player Last Stolen From Self Container13Player Last Stealing (Global)14Player with Highest Score15Player with Lowest Score16Player of Closest Score17Player with Most Aggression18Player with Lowest Aggression19Player Nearest with Aggression20Player Nearest with No Aggression21Player with Most Stored Fruit22Player with Least Stored Fruit23Container Self24Container Closest (excluding self)25Container Farthest (excluding self)26Container of Last Attacker27Container of Last Global Attacker28Container of Most Stored Fruit29Container of Nearest Dead Player30Container of Nearest Dead Player (with Food)31Container of Highest Aggression Player32Container of Lowest Aggression Player33Container of Player with Closest Rank34Fruit Nearest35Fruit Farthest36Fruit Nearest Self Container37Fruit (Random)38Tree Nearest
0Health Percentage1Hunger Percentage2Stamina Percentage3Current Rank4Stored Food Count5Distance to Nearest Player6Distance to Nearest Aggressive Player7Distance to Last Attacker8Distance to Nearest Container9Distance to Nearest Fruit Tree10Player Count Remaining11Current Simulation Time12Max Simulation Time13Simulation Time Remaining14Delta time15Fixed delta time16Pi17Self Gathers18Self Food Consumed19Self Steals20Self Damage Dealt21Self Kills22Self Aggression Level23Available Fruit Count24Player Count Without Stored Food25Average Player Health26Average Player Hunger27Average Player Stamina28Average Player Aggression Level29Total Possible Fruit30Distance Traveled
0Is Carrying Resource1Container has Health2Container Was Attacked3Container Was Stolen From4Self Was Attacked
Parking Simulation
Overview
The goal of the simulation is to navigate the car to a target parking stall and have it sit inside the stall for 3 seconds. After that, the player progresses to a new level with the same goal but from a different local position and with a different target parking stall.
Alignment to the stall is encouraged but not required. Colliding with any objects will reset the player to the starting position.
Requirements
- You must drive your car via the destination node:
ModularUniformController(...) - Use
ConstructModularUniformProperties(...)/InitializeParking(...)to set cosmetics - Sensors are built from
Spherecast(...)→CarRaycasts(...)→HitInfo(...) - The same modular car stack (controller, sensors, Autosteer / Autothrottle) is shared with Demo Derby and RacingV2. RacingV2 adds race-specific getters and uses
ConstructRacingV2Propertiesfor cosmetics + stats — see RacingV2 Simulation.
Nodes
| Node | Purpose | Inputs | Outputs | Notes |
|---|---|---|---|---|
ModularUniformController(throttle, steering, brake) |
Sends inputs to the car to control it | — | Destination node | |
ConstructModularUniformProperties(name, country, skinColor, bodyStyle, hairStyle, hairColor, facialHairStyle, carColor, outfitUrl) |
Sets cosmetic options for this car |
|
— | Destination node |
Spherecast(radius, distance) |
Defines the radius and travel distance used for spherecast sensors |
|
— | |
CarRaycasts(spherecast) |
Sends 8 sensors out around the car (spherecasts) |
|
|
— |
HitInfo(raycastHit) |
Extracts collision info from a RaycastHit |
|
— | |
ParkingGetTransform(value) |
Selection of Transform options that represent a current location in the simulation. value may be a dropdown index (0, 1, …) or one of the exact labels shown in the options column; the saved graph stores the dropdown index in the node’s modifier field. |
— |
|
Self, Target Parking Stall |
ParkingGetFloat(value) |
Selection of number-based options that represent a current simulation parameter | — |
|
Speed, Distance to Stall (based on pathfinding), Target Stall Width, Target Stall Depth, Current Level, Fail Count, Current Simulation Time, Max Simulation Time, Delta Time, Fixed Delta Time, Pi, Signed Speed (forward +, reverse −) |
ParkingGetBool(value) |
Selection of True/False options that represent a current simulation parameter | — |
|
Is Partially in Target Parking Stall, Is Fully in Target Parking Stall |
InitializeParking(...) (convenience helper)
Same nine positional arguments as ConstructModularUniformProperties(...), but applies them as a convenience initialization helper.
Details
Tips
- Raycasts originate from the car’s center. Approx width 6, length 10.8.
- Stepping on the brake while setting throttle to
-1will almost instantly stop the car. - Shared with Demo Derby and RacingV2:
ModularUniformController,Spherecast→CarRaycasts→HitInfo,Autosteer,Autothrottle. Parking cosmetics useConstructModularUniformProperties/InitializeParking(not RacingV2’s stat budget properties node).
Marking the bot as LLM-driven
See the top-level Marking your bot as LLM-driven section. The same props.data["modifier"] = "True" recipe applies to InitializeParking, InitializeDemoDerby, and ConstructModularUniformProperties — they all return the same UniformModularCarProperties node.
Demo Derby Simulation
Overview
Maximize damage to other cars. Collisions deal damage; vulnerable parts change handling (front wheels steering; rear wheels, engine, driveshaft acceleration). At 0 engine health a car explodes and damages nearby cars.
Requirements
- Same modular car stack as Parking / RacingV2:
- Drive with
ModularUniformController(...) - Cosmetics with
ConstructModularUniformProperties(...)/InitializeParking(...)/InitializeDemoDerby(...) - Sensors via
Spherecast(...)→CarRaycasts(...)→HitInfo(...) - Optional
Autosteer/Autothrottle(also used by RacingV2)
- Drive with
Nodes
| Node | Purpose | Inputs | Outputs | Options / Notes |
|---|---|---|---|---|
InitializeDemoDerby(name, country, skinColor, bodyStyle, hairStyle, hairColor, facialHairStyle, carColor, outfitUrl) |
Convenience helper to set derby cosmetics (delegates to InitializeParking) |
See ConstructModularUniformProperties(...) in Parking Simulation |
— | Returns UniformModularCarProperties; set props.data["modifier"] = "True" to mark as LLM-driven |
DemoDerbyGetTransform(value) |
Get key transforms used in the derby sim | — |
|
Values:
0 self1 fixed ref (inspector)2 random pathable waypoint |
DemoDerbyGetCar(mode, index_float=None) |
Select a car reference by mode (nearest, ranked, etc.) | — |
|
Modes:
0 by index1 by rank2 self3 nearest car4 furthest car5 lowest health car6 highest health car7 last damaged car8 nearest active9 furthest active10 nearest disabled11 furthest disabled12 nearest with disabled steering (rear may still drive)13 furthest with disabled steering14 nearest AI-authored (active)15 lowest health AI16 highest health AI17 nearest human-authored (active)18 lowest health human19 highest health human20 highest ranked21 lowest ranked22 nearest ranked (rank neighbor of self)23 highest ranked (not immobilized)24 highest ranked (immobilized)25 lowest ranked (not immobilized)26 lowest ranked (immobilized) |
CarGetPart(mode, car) |
Pick a part of a car (aim point, weakpoint, etc.) |
|
|
Modes:
0 average of all parts1 nearest part2 weakest part3 nearest crucial part4 WheelFL5 WheelFR6 WheelRL7 WheelRR8 AxleFL9 AxleFR10 AxleRL11 AxleRR12 Engine13 Driveshaft14 SuspensionFL15 SuspensionFR16 SuspensionRL17 SuspensionRR18 Hood19 Trunk20 BumperFront21 BumperRear22 LicensePlateFront23 LicensePlateRear24 FenderFL25 FenderFR26 DoorL27 DoorR28 DoorRL29 DoorRR30 TurnSignalFL31 TurnSignalFR32 HeadlightL33 HeadlightR34 TaillightL35 TaillightR36 WindshieldWipers |
CarInfo(car) |
Multi-output car info helper |
|
Notes | |
GetCarFromTransform(transform) |
Convert a Transform into a Car reference |
|
|
— |
Autosteer(goal) |
Steer toward a world-space goal |
|
|
— |
Autothrottle(goal, desired_speed) |
Throttle toward a goal with a target speed |
|
— |
Details
- Transform vs Vector3: Many helpers output a Transform (type
Transform) which is not a Vector3 position. In this library, transforms are represented asNodeobjects and do not have Unity-style fields like.Position/.position. - How to get a position Vector3 from a Transform: use
RelativePosition(transform_node, "Self")(returns aVector3). - Bad (will error):
goal = CarGetPart(0, car).PartTransform.Position - Good:
part = CarGetPart(0, car)part_tf, _health = CarGetPart(0, car)goal = RelativePosition(part_tf, "Self")
0by index ·1by rank ·2self3nearest car ·4furthest car5lowest health car ·6highest health car7last damaged car8nearest active ·9furthest active10nearest disabled ·11furthest disabled12nearest with disabled steering (rear may still drive) ·13furthest with disabled steering14nearest AI-authored (active) ·15lowest health AI ·16highest health AI17nearest human-authored (active) ·18lowest health human ·19highest health human20highest ranked ·21lowest ranked ·22nearest ranked (rank neighbor of self)23highest ranked (not immobilized) ·24highest ranked (immobilized)25lowest ranked (not immobilized) ·26lowest ranked (immobilized)
- Modes
0–3: average of all parts; nearest part; weakest part; nearest crucial part - Modes
4–36select a specific part:4WheelFL5WheelFR6WheelRL7WheelRR8AxleFL9AxleFR10AxleRL11AxleRR12Engine13Driveshaft14SuspensionFL15SuspensionFR16SuspensionRL17SuspensionRR18Hood19Trunk20BumperFront21BumperRear22LicensePlateFront23LicensePlateRear24FenderFL25FenderFR26DoorL27DoorR28DoorRL29DoorRR30TurnSignalFL31TurnSignalFR32HeadlightL33HeadlightR34TaillightL35TaillightR36WindshieldWipers
- CarGetPart: only output
Transform1currently wires up correctly. Using the health output (published asFloat2) in another node currently raisesKeyError: 'Float2'. - CarInfo: only output
Transform1currently wires up correctly. UsingVector32/Bool3/Bool4/Float5/Float6in another node currently raisesKeyError(commonlyVector32/Bool3/Bool4/Float5/Float6). - The same limitation applies in RacingV2 graphs that use
CarInfo/CarGetPart— prefer.CarTransform/.PartTransformplusRacingV2GetFloat/ sensors instead of multi-output component reads.
This is the smallest end-to-end derby bot. It chases the nearest active car, aims for that car's nearest crucial part, commits to the throttle, and brakes if something is right in front of the bumper but the real target is still far away.
from AIGamePyLibrary import *
# 1. Cosmetics + LLM flag (positional args; capture the returned node).
props = InitializeDemoDerby(
"MyBot", # name
"Gemini", # country (LLM persona names are valid)
"Tan", # skin color
0, # body style
2, # hair style
"Brown", # hair color
0, # facial hair style
"Blue", # car color
"", # custom outfit URL (empty)
)
props.data["modifier"] = "True" # mark this bot as LLM-driven
# 2. Self position (Vector3) for distance checks.
self_pos = RelativePosition(DemoDerbyGetTransform(0), "Self")
# 3. Pick a target: nearest active opponent, aim at its nearest crucial part.
target = DemoDerbyGetCar(8) # 8 = nearest active
target_tf, _target_part_health = CarGetPart(3, target) # 3 = nearest crucial part
goal = RelativePosition(target_tf, "Self")
# 4. Forward sensor for emergency braking against scenery.
sensor = Spherecast(1.2, 12.0)
_, ray_f, *_ = CarRaycasts(sensor)
front_hit, front_dist = HitInfo(ray_f)
goal_dist = Distance(self_pos, goal)
panic = front_hit & (front_dist < 2.5) & (goal_dist > 10.0)
brake = ConditionalSetFloat(panic, 1.0, 0.0)
# 5. Drive: Autosteer / Autothrottle do the heavy lifting.
ModularUniformController(
Autothrottle(goal, 25.0), # throttle
Autosteer(goal), # steering
brake, # brake
)
# 6. Save (without this, nothing is exported).
SaveData("MyBot", "auto")- Raycasts originate at the car center (rough width 6, length 10.8, same as Parking / RacingV2).
- JSON node id for auto throttle is
Autothrottle(notAutoThrottle). - Shared modular stack also powers RacingV2; use
RacingV2Get*for race state andInitializeRacingV2for cosmetics + stats.
Soccer Simulation
Overview
Team soccer (4 players per team graph). Control each player with move-to, sprint, and interact (shoot / tackle). Match flow includes kickoff, play, goals, overtime, and whistle (stale ball).
Save folder: Soccer/
Requirements
- Drive players with
SoccerController(player, moveTo, sprint, interact)for players1–4(Unity keysSoccerController1…4) - Optional sensors:
SoccerPlayerSensors(player, spherecast)→ eight RaycastHits labeled A–H on the Player Sensor node - Team cosmetics / faceoff:
ConstructSoccerProperties(...)/InitializeSoccer(...) - World state:
SoccerGetBool/SoccerGetFloat/SoccerGetVector3/SoccerGetTransform
Nodes
| Node | Purpose | Inputs | Outputs | Options / Notes |
|---|---|---|---|---|
SoccerController(player, moveTo, sprint, interact) |
Controls team player player (1–4) |
— | Destination. Hold interact to charge a shot (with ball) or attempt tackle (without). See Details. | |
SoccerPlayerSensors(player, spherecast) |
Eight-way spherecasts around player player (1–4) |
RaycastHit1…8 (letters A–H) |
Aligns with the Player Sensor graphic | |
ConstructSoccerProperties(name, country, faceoff1..4) |
Team name, country, and faceoff positions | String, Country, Vector3 ×4 | — | Destination. Or use InitializeSoccer(...) |
SoccerGetBool(value) |
Bool world state | — | Bool1 |
Index or label — see all values |
SoccerGetFloat(value) |
Float world state | — | Float1 |
Index or label — see all values |
SoccerGetTransform(value) |
Key transforms | — | Transform1 |
Index or label — see all values |
SoccerGetVector3(value) |
Directions, landmarks, open-player picks | — | Vector31 |
Index or label — see all values |
Details
A tackle contest subtracts the stamina delta between the tackling player and the ball carrier from both players. After that drain, if the tackling player has more stamina (or equal), they win the ball.
If a player has the ball and there is shot charge, setting interact (Bool2) to false releases the shot — the ball is kicked in the direction of that frame’s movement input.
Hold interact while with the ball to charge; without the ball, interact attempts a tackle / pickup.
SoccerGetVector3 options that search clear / directional views check the 8 player spherecast directions and return the first valid direction meeting the criteria (or null if none). Letters match the Player Sensor graphic (A–H).
Search order prioritizes the team’s attacking direction:
- Home: E, C, H, B, G, A, F, D
- Away: D, F, A, G, B, H, C, E
- Opening kickoff receiving team is random
- After a goal, the team that was scored on receives the next kickoff
- In extra time, the receiving team is the opposite of who kicked off at match start
- On whistle (stale ball / no meaningful movement after time), kickoff flips to the opposite of who last received a kickoff
Live values from the Soccer scene / player prefab. Several are also exposed as SoccerGetFloat labels (listed in parentheses).
Field
- Field width (sideline to sideline): 50 (
"Field Width") - Field depth (goal line to goal line): 80 (
"Field Depth") - Kickoff / center circle radius: 7.25 (
"Kickoff Circle Radius") - Goal width: 11.4 (
"Goal Width") - Goal height: 4 (
"Goal Height") - Players per team: 4
Movement
- Walk / run speed: 7
- Sprint speed: 8 (requires available stamina)
Stamina
- Max stamina: 100
- Sprint consume: 0.15 per simulation tick while sprinting
- Regen rate: 5 stamina per second (after the regen delay)
- Default regen delay (cooldown): 1 second
- Tackle / contested-steal regen delay: 1.5 seconds (applied to both players in the contest)
Match & interaction
- Match duration: 180 seconds of playing time
- Kickoff restriction delay: 1 second (or until first touch)
- Player interact / tackle / pickup radius: 1.75 (
"Player Interact Radius") - Shot charge rate: 0.1 per tick; min charge to shoot 0.1; max charge time 2 s
- Shot strength range: 0.5–15 (plus max lift 3)
- Shot cooldown: 0.5 s
- Pickup charge delay after gaining the ball: 0.3 s
- Stale-ball whistle: ball stays within 2.5 of its anchor for 5 s
Ball physics (from SoccerBall prefab + project Physics settings)
- Mass: 0.45
- Linear damping: 0
- Angular damping: 0
- Use gravity: yes (project gravity is (0, −20, 0) — heavier than Unity’s default −9.81)
- Max linear speed clamp: 30 m/s
- Rigidbody rotation: frozen (rolling is visual only)
- Collision detection: Discrete
- Sphere collider radius: ~0.406 world units (prefab radius 0.4515 × scale 0.9)
- Physics Material on ball / typical field colliders: none assigned
- Project default material is also none, so contacts use Unity’s built-in defaults: dynamic/static friction 0.6, bounciness 0
- Bounce threshold: 2
- Shot impulse: release sets
velocity = force / mass(then clamped to max speed); upward shot bias component 3 - Pickup lockout after shot (shooter only): 0.125 s; after possession exchange: 0.25 s
Pass a dropdown index (0, 1, …) or the exact Unity label string below. Order matches Unity and DROPDOWN_OPTIONS in data.py.
"Team Has Ball", "Opponent Has Ball", "Is Ball Loose", "Team Player 1 Has Ball", "Team Player 2 Has Ball", "Team Player 3 Has Ball", "Team Player 4 Has Ball", "Opponent Player 1 Has Ball", "Opponent Player 2 Has Ball", "Opponent Player 3 Has Ball", "Opponent Player 4 Has Ball", "Is Ball Nearby Team Player 1", "Is Ball Nearby Team Player 2", "Is Ball Nearby Team Player 3", "Is Ball Nearby Team Player 4", "Is Ball Nearby Opponent Player 1", "Is Ball Nearby Opponent Player 2", "Is Ball Nearby Opponent Player 3", "Is Ball Nearby Opponent Player 4", "Is Team Player 1 Closest Teammate to Ball", "Is Team Player 2 Closest Teammate to Ball", "Is Team Player 3 Closest Teammate to Ball", "Is Team Player 4 Closest Teammate to Ball", "Is Opponent Player 1 Closest Opponent to Ball", "Is Opponent Player 2 Closest Opponent to Ball", "Is Opponent Player 3 Closest Opponent to Ball", "Is Opponent Player 4 Closest Opponent to Ball", "Is Team Player 1 Open", "Is Team Player 2 Open", "Is Team Player 3 Open", "Is Team Player 4 Open", "Is Opponent Player 1 Open", "Is Opponent Player 2 Open", "Is Opponent Player 3 Open", "Is Opponent Player 4 Open", "Is Kickoff", "Is Team Kicking off", "Is Opponent Kicking off", "Team Is Winning", "Opponent Is Winning", "Team Scored Last Point", "Opponent Scored Last Point", "Ball On Team Side", "Ball On Opponent Side", "Is Ball Headed Towards Team Goal", "Is Ball Headed Towards Opponent Goal", "Is Home Team", "Is Away Team", "Is Active Graph"
"Team Score", "Opponent Score", "Team Shots", "Opponent Shots", "Team Possession %", "Opponent Possession %", "Team Attacking %", "Opponent Attacking %", "Ball Speed", "Player Interact Radius", "Player With Ball Shot Charge %", "Ball Carrier Stamina", "Ball Carrier Shot Charge", "Teammate 1 Shot Charge", "Teammate 2 Shot Charge", "Teammate 3 Shot Charge", "Teammate 4 Shot Charge", "Field Width", "Field Depth", "Kickoff Circle Radius", "Goal Width", "Goal Height", "Team Player 1 Stamina", "Team Player 2 Stamina", "Team Player 3 Stamina", "Team Player 4 Stamina", "Distance from Team Player 1 to nearest Opponent", "Distance from Team Player 2 to nearest Opponent", "Distance from Team Player 3 to nearest Opponent", "Distance from Team Player 4 to nearest Opponent", "Opponent Player 1 Stamina", "Opponent Player 2 Stamina", "Opponent Player 3 Stamina", "Opponent Player 4 Stamina", "Opponent Nearest Teammate Player 1 Stamina", "Opponent Nearest Teammate Player 2 Stamina", "Opponent Nearest Teammate Player 3 Stamina", "Opponent Nearest Teammate Player 4 Stamina", "Stamina of last defending opponent", "Current Simulation Time", "Max Simulation Time", "Simulation Time Remaining", "Delta Time", "Fixed Delta Time", "Pi"
"Ball", "Team Player 1", "Team Player 2", "Team Player 3", "Team Player 4", "Opponent Player 1", "Opponent Player 2", "Opponent Player 3", "Opponent Player 4", "Teammate Nearest Team Player 1", "Teammate Nearest Team Player 2", "Teammate Nearest Team Player 3", "Teammate Nearest Team Player 4", "Opponent Nearest Team Player 1", "Opponent Nearest Team Player 2", "Opponent Nearest Team Player 3", "Opponent Nearest Team Player 4", "Team Goal Center", "Team Goal Left Post", "Team Goal Right Post", "Opponent Goal Center", "Opponent Goal Left Post", "Opponent Goal Right Post", "Opponent Nearest Team Goal", "Opponent Nearest Opponent Goal", "Teammate Nearest Team Goal", "Teammate Nearest Opponent Goal"
"Ball Velocity", "Clear direction from team carrier", "Backwards clear direction from team carrier", "Clear direction from team carrier (avoid goal lines)", "Clear direction from team carrier (avoid sidelines)", "Clear direction from team carrier (avoid all walls)", "Upper Corner Home Side", "Lower Corner Home Side", "Upper Midfield", "Lower Midfield", "Upper Corner Away Side", "Lower Corner Away Side", "Upper Corner Opposing Side", "Lower Corner Opposing Side", "Upper Corner Team Side", "Lower Corner Team Side", "Center Field", "Get nearest open teammate", "Get furthest open teammate", "Get most open teammate", "Get nearest open opponent", "Get furthest open opponent", "Get most open opponent", "Direction of clear teammate from Teammate 1", "Direction of clear teammate from Teammate 2", "Direction of clear teammate from Teammate 3", "Direction of clear teammate from Teammate 4", "Direction of clear teammate from Opponent 1", "Direction of clear teammate from Opponent 2", "Direction of clear teammate from Opponent 3", "Direction of clear teammate from Opponent 4", "Direction of ball from Teammate 1", "Direction of ball from Teammate 2", "Direction of ball from Teammate 3", "Direction of ball from Teammate 4", "Direction of ball from Opponent 1", "Direction of ball from Opponent 2", "Direction of ball from Opponent 3", "Direction of ball from Opponent 4", "Direction of team goal from Teammate 1", "Direction of team goal from Teammate 2", "Direction of team goal from Teammate 3", "Direction of team goal from Teammate 4", "Direction of opponent goal from Teammate 1", "Direction of opponent goal from Teammate 2", "Direction of opponent goal from Teammate 3", "Direction of opponent goal from Teammate 4", "Clear direction from Teammate 1", "Clear direction from Teammate 2", "Clear direction from Teammate 3", "Clear direction from Teammate 4", "Direction of teammate from Team Player 1", "Direction of teammate from Team Player 2", "Direction of teammate from Team Player 3", "Direction of teammate from Team Player 4"
- During kickoff restriction, only the kicking-off team gets graph control; non-kickoff players are kept outside the center circle until first touch / delay expires.
- Convert transforms to positions with
RelativePosition(transform, "Self").
RacingV2 Simulation
Overview
Lap racing on additive V2 tracks using the modular car stack. Default 3 laps; cars can DNF on no waypoint progress; modular part damage still applies.
Save folder: RacingV2/
Requirements
- Drive with
ModularUniformController(throttle, steering, brake)(same as Parking / Demo Derby) - Cosmetics + 20-point stats:
ConstructRacingV2Properties(...)/InitializeRacingV2(...)(notUniformModularCarProperties) - Sensors:
Spherecast→CarRaycasts→HitInfo(shared stack) - Optional helpers:
Autosteer,Autothrottle,CarGetPart,CarInfo(same multi-output wiring caveat as Demo Derby) - Race state:
RacingV2GetFloat/RacingV2GetBool/RacingV2GetCar/RacingV2GetWaypoint/RacingV2Waypoint
Nodes
| Node | Purpose | Inputs | Outputs | Options / Notes |
|---|---|---|---|---|
InitializeRacingV2(..., speed, turn, health) |
Cosmetics + Stat1/2/3 (20-point budget → speed / turn / health) | Same cosmetics as Parking, plus three Stat inputs |
— | Unity key ConstructRacingV2Properties |
RacingV2GetFloat(value) |
Speed, waypoints, rank, laps, sim time, … | — | Float1 |
Index or label (see DROPDOWN_OPTIONS) |
RacingV2GetBool(value) |
Grounded / disabled / sim started | — | Bool1 |
Is Grounded, Is Disabled, Simulation started |
RacingV2GetCar(mode, index_float=None) |
Select a car (modes 0–26, same shape as DemoDerbyGetCar) | Optional Float for by-index / by-rank | Car1 |
Rank uses race scoreboard |
RacingV2GetWaypoint(value, index_float=None) |
Next / Previous / By index / Start waypoint | Optional Float for By index | Waypoint1 |
Feed into RacingV2Waypoint |
RacingV2Waypoint(mode, waypoint, ref=None) |
Resolve a waypoint to a world point | Waypoint (+ optional Transform for Nearest) | .Position (Vector3), .Index (Float) |
Center / Left / Right / Nearest point |
Shared modular nodes are documented under Parking Simulation and Demo Derby Simulation (ModularUniformController, CarRaycasts, Autosteer, Autothrottle, …).
Details
ConstructRacingV2Properties takes Stat1 (speed), Stat2 (turn), Stat3 (health) with a sequential 20-point budget (typical mapping: max speed ~20–60, turn ~0.5–2.0, health fraction of baseline).
Speed, Signed Speed (forward +, reverse −), waypoint indices/distances, Current race rank, Number of competitors, lap times, Current lap / Total laps, sim time, Delta Time, Pi, …
- Prefer
Autosteer(goal)+Autothrottle(goal, speed)towardRacingV2Waypoint(...).Position. - Avoid
CarInfo/CarGetPartnon-Transform outputs (same KeyError limitation as Demo Derby). - Older
Racing.unity/ Kart nodes are a different stack — use RacingV2 helpers for this scene.
from AIGamePyLibrary import *
props = InitializeRacingV2(
"Racer", "United States of America", "Tan", 0, 0, "Brown", 0, "Red", "",
7, 7, 6,
)
wp = RacingV2GetWaypoint("Next waypoint")
goal = RacingV2Waypoint("Center", wp).Position
ModularUniformController(Autothrottle(goal, 30.0), Autosteer(goal), Float(0.0))
SaveData("RacingV2/AIComp_Data/Saves/racer.txt", "grid")Volleyball Simulation
Overview
Volleyball bots control a slime character to move and jump based on the current world state (self/opponent/ball positions and velocities, scores, etc.).
Requirements
- Drive the slime via the destination node:
SlimeController(targetPosition, jumpCondition)- Read world state via sim-prefixed helpers:
VolleyballGetVector3/VolleyballGetTransform/VolleyballGetBool/VolleyballGetFloatNodes
Node Purpose Inputs Outputs Options / Notes VolleyballGetVector3(value)World-space vectors (Unity node type id: SlimeGetVector3)—
Vector31— The resulting value from the selectionValues VolleyballGetTransform(value)Transform references (convert to Vector3 via RelativePosition(tf, \"Self\"))—
Transform1— The selected valueValues VolleyballGetBool(value)Boolean state —
Bool1— The selected valueValues VolleyballGetFloat(value)Scalar state —
Float1— The selected valueValues InitializeSlime(name, color, country, speed, acceleration, jump)Initialize your slime bot with the specified properties See ConstructSlimeProperties(...)below— Convenience helper SlimeController(targetPosition, jumpCondition)Drive the slime (movement + jump) — Destination node ConstructSlimeProperties(name, color, country, speedStat, accelerationStat, jumpStat)Low-level slime cosmetics + stat initialization — Destination node RelativePosition(transform, direction)Convert a Transform reference into a world-space Vector3
Transform1— Transform to query
Vector31— World-space positionSelf,Self + Forward,Self + Backward,Self + Left,Self + Right,Self + Up,Self + Down,Forward,Backward,Left,Right,Up,Down,WorldStat(value)Create a Stat node (used for slime properties) —
Stat1— The statvaluecan beintorstrColor(value)Create a Color node —
Color1— The selected colorSee values Country(value)Create a Country node —
Country1— The selected country / personaSee values Details
"Self Position","Self Velocity","Opponent Position","Opponent Velocity","Ball Position","Ball Velocity"
"Self","Opponent","Ball","Self Team Spawn","Opponent Team Spawn"
"Self Can Jump","Opponent Can Jump","Ball Is Self Side"
"Delta time","Fixed delta time","Gravity","Pi","Simulation duration","Team score","Opponent score","Ball touches remaining"
"Self","Self + Forward","Self + Backward","Self + Left","Self + Right","Self + Up","Self + Down","Forward","Backward","Left","Right","Up","Down","World"
"Auburn","Black","Blonde","Blue","Brown","Dark Brown","Dark Green","Green","Hot Pink","Light Blue","Light Grey","Medium Grey","Orange","Pink","Purple","Red","Tan","White","Yellow"See the full list in the Country node docs above (same list used across simulations).
- There are no Unity-style dotted shortcuts. Always use the sim-prefixed helpers.
- The unprefixed aliases (
GetVector3/GetTransform/GetBool/GetFloat) are Volleyball-only backward-compat; prefer the explicitVolleyballGet*names.- Transform → world position:
RelativePosition(transform_node, "Self").
SaveData Function
SaveData(filePath, layout="auto", pruneUnusedNodes=True, keepPosition=True, optimize="normal", verbose=False)- Saves the AI data to a JSON file that can be imported into Unity
filePath: Path to save the filelayout: Layout mode"auto"- Topological layout (recommended)"grid"- Grid-based layout"single"- All nodes at origin"hidden"- Nodes positioned off-screenNone- No layout changes
pruneUnusedNodes: Remove nodes that aren't connected (default: True)keepPosition: Preserve manually set node positions (default: True)optimize: How hard to compile the graph before saving (see below)verbose: Print optimiser strip/prune counts (default: False)
Every node in the saved graph evaluates every tick in-engine, so fewer nodes
means both a smaller file and less per-tick work. Pass optimize= to
SaveData to choose how aggressively to prune:
| Value | What it does |
|---|---|
"normal" (default) |
Safe prune only: drop unreachable nodes and SetVariable writes that no GetVariable reads. Keeps every Debug* / TimePlot sink so the graph stays observable while you develop. |
"release" |
Also strips every Debug* / TimePlot sink, then re-prunes to a fixpoint so anything that only fed debug output is removed too. Smallest graph; fewest per-tick evaluations. |
# Development: keep DebugDraw / TimePlot so you can inspect the graph
SaveData("MyBot_debug.txt", layout="grid", optimize="normal")
# Shipping: strip debug sinks and dead subgraphs that only fed them
SaveData("MyBot.txt", layout="grid", optimize="release", verbose=True)Assumption: "release" assumes debug/plot nodes are outputs only — they
must not also feed a controller or decision. That holds for typical bots, but
verify before shipping if your graph wires debug helpers into gameplay logic.
Positional layout still works (SaveData("MyBot", "grid")); use the keyword
when you want release:
SaveData("MyBot.txt", "grid", optimize="release")There is no general decompiler from Unity graph JSON back into Python. You do not need one: the optimiser works on the JSON graph itself.
from AIGamePyLibrary import OptimizeFile, LoadData, SaveData
# Compact an editor-made / already-exported bot (default optimize="release")
OptimizeFile("Haialand-v2.txt", "Haialand-v2_release.txt", verbose=True)
# Or overwrite in place
OptimizeFile("MyBot.txt", verbose=True)
# Or load → tweak in memory → save yourself
LoadData("MyBot.txt")
SaveData("MyBot_compact.txt", layout=None, optimize="release")After Lean prepare, also drop fields the soccer sim does not need to decide:
- connection
sID/ instance IDs - port
nodeSID - all
serializableRectTransformlayout chrome - serialize/color flags, empty
modifier/ownerFunctionSID
OptimizeFile("Titanium.txt", "Titanium_leaner.txt", optimize="normal",
pruneUnusedNodes=False, leaner=True)
# or while building:
SaveData("MyBot.txt", layout="grid", optimize="release", leaner=True)Do not drop real modifier values or port polarity — those break load /
dropdown resolution. leaner graphs may not round-trip cleanly through the
Unity node editor (layout/chrome removed).
from AIGamePyLibrary import *
# Initialize bot
InitializeSlime("MyBot", "Blue", "Canada", 6, 4, 3)
# Pull world state from the graph helpers (no dotted accessors anywhere).
ball_position = VolleyballGetVector3("Ball Position")
self_position = VolleyballGetVector3("Self Position")
# Calculate direction to ball
directionToBall = ball_position - self_position
distanceToBall = Magnitude(directionToBall)
# Normalize direction and add offset
normalizedDir = Normalize(directionToBall)
targetOffset = normalizedDir * 0.3
moveTo = ball_position + targetOffset
# Jump when close to ball and ball is above us (component access on Vector3 Nodes)
ballAbove = ball_position.y > self_position.y
closeToBall = distanceToBall < 2.0
jumpCondition = closeToBall & ballAbove
# Control the slime
SlimeController(moveTo, jumpCondition)
# Save with auto layout
SaveData("my_bot.txt", "auto")- Use Python operators: Instead of calling
AddFloats(a, b), usea + bfor cleaner code - Node caching: Functions automatically cache nodes with the same inputs for efficiency
- Layout options: Use
"auto"for clean topological layouts,"grid"for grid-based layouts - Debugging: Use
Debug(value)to inspect node values during development - Vector components: Access vector components via
.x,.y,.zproperties on Vector3 nodes
The SaveData function generates a JSON file that can be imported into Unity for use in AIA's games. The file contains all the node connections and logic you've defined in Python.
This section is the “type dictionary” that port keys link to (for example: clicking Float1 jumps you here).
RaycastHit
- Type: RaycastHit
- Ports:
RaycastHit1,RaycastHit2, ... - Meaning: A ray/spherecast hit result container (used by sensors).
Spherecast
- Type: Spherecast
- Ports:
Spherecast1,Spherecast2, ... - Meaning: A sensor definition that configures the size/length of spherecast sensors.
Any
- Type: Any
- Ports:
Any1,Any2, ... - Meaning: Generic “wildcard” type. Carries through whatever concrete type you connect.
Vector3
- Type: Vector3
- Ports:
Vector31,Vector32, ... - Meaning: A 3D vector. Supports
.x,.y,.zcomponent access in the graph.
Transform
- Type: Transform
- Ports:
Transform1,Transform2, ... - Meaning: A Unity Transform reference (position/rotation container). Use
RelativePosition(transform, "Self")to convert to a world-space Vector3.
SurvivalState
- Type: SurvivalState
- Ports:
SurvivalState1,SurvivalState2, ... - Meaning: Survival simulation state enum.
SurvivalEmote
- Type: SurvivalEmote
- Ports:
SurvivalEmote1,SurvivalEmote2, ... - Meaning: Survival simulation emote enum.
Car
- Type: Car
- Ports:
Car1,Car2, ... - Meaning: Car reference for Demo Derby / RacingV2 (selected via
DemoDerbyGetCar(...)/RacingV2GetCar(...)or derived from a Transform).
Notes for LLM Authors
The car / kart Properties node carries an isLLM flag that is not exposed in the Unity inspector — it can only be set by the PyLib compiler. At runtime Unity reads it from the node's modifier field during Initialize(), writes it onto the resulting properties (KartProperties / modular car properties), and applies it to the spawned Player (Player.IsLLM = true).
To set it, capture the Properties node returned by any of the helpers and assign its modifier after construction. Accepted values: "True" / "False" or "1" / "0" (anything else falls back to False).
from AIGamePyLibrary import *
# Works for InitializeDemoDerby, InitializeParking, and ConstructModularUniformProperties —
# they all return the same UniformModularCarProperties node.
# For RacingV2, use InitializeRacingV2 / ConstructRacingV2Properties the same way (modifier on the returned node).
props = InitializeDemoDerby(
"MyBot", "United States of America", "Tan",
0, 0, "Brown", 0, "Red", "",
)
props.data["modifier"] = "True" # mark this car as LLM-drivenisLLM is persisted in the saved JSON through the standard modifier field, so the graph round-trips through Unity without losing the flag. Equivalent behavior is wired up on the C# side for ConstructKartProperties (Kart-style simulations) and RacingV2 properties — same modifier accepted values.
This section consolidates everything an LLM (or anyone new to the library) needs to avoid the most common mistakes. Read it in full before writing a script.
🛑 Before you submit a bot, scan it for these six accessor patterns and delete every one of them — they all currently crash in
ConnectPortswith aKeyErrorand are the #1 thing we keep seeing LLMs burn on:
CarInfo(...).Velocity→KeyError: 'Vector32'CarInfo(...).IsAI→KeyError: 'Bool3'CarInfo(...).IsImmobile→KeyError: 'Bool4'CarInfo(...).Health→KeyError: 'Float5'CarInfo(...).Rank→KeyError: 'Float6'CarGetPart(...).HealthPercent→KeyError: 'Float2'That includes the sneaky tuple-unpacking form (
car_tf, velocity, is_ai, is_immobile, health, rank = CarInfo(car)— the unpacked vars are the same broken nodes) and any arithmetic/comparison that funnels them into another node (Magnitude(v),v * dt,health < 50,ConditionalSetFloat(is_immobile, ...), etc.). Only.CarTransformand.PartTransformare safe. See Multi-output component accessor bug for replacements.
This is a graph compiler, not a runtime game SDK. Your script does not drive the car/slime/Aialander frame-by-frame. It builds a static node graph once, and SaveData(...) writes that graph to JSON. Unity loads the JSON and re-evaluates the graph every tick.
Concretely:
- Every value you compose (
VolleyballGetVector3("Self Position"),Distance(...),props, sensor outputs, etc.) is aNodeobject, not a number, string, list, or dict. - Plain Python
if/while/for,min/max/sorted,lambda, list/dict indexing, andimport mathcalls do not become nodes. They run once at compile time and are gone. Use the graph equivalents:ConditionalSetFloat/ConditionalSetVector3/ConditionalSetBoolfor branching,Operation(x)for math,DemoDerbyGetCar/SurvivalGetTransformto "pick" entities, etc. - Nodes have no Unity-style dotted accessors. There is no
something.Position/something.Velocity/something.Transform/something.DeltaTimeanywhere in this library. To read world state you always call a simulation-prefixed helper that matches the Unity asset for that sim:- Volleyball →
VolleyballGetVector3(...)/VolleyballGetTransform(...)/VolleyballGetBool(...)/VolleyballGetFloat(...)(Vector3 node type in Unity:SlimeGetVector3; other nodes:VolleyballGetTransform, etc. — seeAssets/_Nodes/). - Survival →
SurvivalGetTransform(...)/SurvivalGetFloat(...)/SurvivalGetBool(...). - Parking →
ParkingGetTransform(...)/ParkingGetFloat(...)/ParkingGetBool(...). - Demo Derby →
DemoDerbyGetTransform(...)/DemoDerbyGetCar(...)/CarGetPart(...).PartTransform/CarInfo(...).CarTransform. - RacingV2 →
RacingV2GetFloat(...)/RacingV2GetBool(...)/RacingV2GetCar(...)/RacingV2GetWaypoint(...)/RacingV2Waypoint(...)plus the shared modular car stack (ModularUniformController,CarRaycasts, …). Properties:InitializeRacingV2/ConstructRacingV2Properties. - Soccer →
SoccerGetBool(...)/SoccerGetFloat(...)/SoccerGetTransform(...)/SoccerGetVector3(...), with per-playerSoccerController(1..4, …)andSoccerPlayerSensors(1..4, …). - The unprefixed aliases (
GetVector3/GetTransform/GetBool/GetFloat) are Volleyball only — they exist purely for backward-compat with old scripts. Using them in any other sim's graph produces the wrong Unity node and won't deserialize correctly. - Custom Functions (
CreateFunction/CustomFunction) are reusable Unity subgraphs: params in, return out viaSetFunctionReturn(fn, body_output)(e.g.Power.Float1→ ReturnAny1In). Body nodes only run when called — not the same as PythoncustomNodes.pyhelpers. See Default Nodes → Organization → Custom Functions. - To turn any
Transformnode into a world-spaceVector3, wrap it inRelativePosition(transform_node, "Self").
- Volleyball →
- Save folders by sim include
Soccer/,RacingV2/,DemoDerby/,Parking/, etc. - The
Initialize*helpers take positional arguments, not keyword arguments. There is noname=,country=,modifier_llm=,save_file=etc. See each simulation's section for the exact signature. - There is no
simobject. Methods likesim.is_active(),sim.get_self_data(),sim.get_opponents(),sim.set_controls(),sim.update()do not exist — if you wrote any of those, you are hallucinating an SDK that isn't here. - The
AIGamePyLibraryimport exposes helpers as free functions, not methods on a module object you treat like asimruntime. (Aialander PyLib is the same project under that friendly name.) - The LLM-driven flag is set on the returned Properties node:
props.data["modifier"] = "True". There is nomodifier_llm=,is_llm=,llm=, orisLLM=keyword argument on any helper. - Your script must end with a call to
SaveData("YourBot", "auto")or nothing is exported.
The most common error LLMs make when writing Demo Derby or RacingV2 bots is using CarInfo(...).IsImmobile, .Velocity, .Health, .Rank, or CarGetPart(...).HealthPercent in ConditionalSetFloat, comparisons, arithmetic, etc.
# These ALL fail with KeyError in ConnectPorts:
is_stuck = CarInfo(self_car).IsImmobile # → 'Bool4'
throttle = ConditionalSetFloat(is_stuck, -1.0, throttle_fwd)
# or: Magnitude(CarInfo(car).Velocity) → 'Vector32'Fix:
- Only use
.CarTransformand.PartTransformfrom these helpers. - For stuck detection use forward raycast sensors (
HitInfo(ray_f)) as shown inGrok.py. - See full details in Multi-output component accessor bug below.
Grok.py (and Claude.py) have been updated with a working sensor-based unstick workaround. Older versions will hit this exact error.
These are the exact mistakes we keep seeing. If your draft does any of them, rewrite it before saving.
| ❌ Don't | ✅ Do |
|---|---|
import AIGamePyLibrary as aig then aig.InitializeDemoDerby(...) (treats helpers as methods on a sim object) |
from AIGamePyLibrary import * and call helpers as free functions |
InitializeDemoDerby(name="X", country="USA", modifier_llm=True, save_file="X") |
props = InitializeDemoDerby("X", "United States of America", "Tan", 0, 0, "Brown", 0, "Red", "") — positional only |
country="USA" / country="UK" / country="South-Korea" |
Use the exact strings from the Country list, e.g. "United States of America", "United Kingdom", "South Korea". Bot personas: "ChatGPT", "Claude", "Deepseek", "Gemini", "Grok", "Llama", "Mistral", "Perplexity", "Qwen" |
Pass modifier_llm=True / is_llm=True / llm=True to any helper |
After init: props.data["modifier"] = "True" (see Marking your bot as LLM-driven) |
Pass save_file="X" to any helper |
Saving is a separate call at the end of the script: SaveData("X", "auto") |
while sim.is_active(): sim.set_controls(...) style runtime loop |
Build the graph once. Unity runs it every tick. There is no loop in your script. |
sim.get_self_data(), sim.get_opponents(), sim.set_controls(), sim.update(), sim.is_active() |
None of these exist. Use DemoDerbyGetTransform, DemoDerbyGetCar, CarInfo, CarRaycasts, ModularUniformController, etc. |
if dist < 50: throttle = 1.0 else: throttle = 0.5 |
throttle = ConditionalSetFloat(dist < 50, 1.0, 0.5) |
min(opponents, key=lambda o: ...) / iterating Python lists of game entities |
There is no Python-side list of opponents. Use selector nodes like DemoDerbyGetCar(8) (nearest active), CarGetPart(3, car) (nearest crucial part), SurvivalGetTransform(3) (player nearest), etc. |
math.atan2(...), math.sqrt(...), math.degrees(...) |
Operation(x) (atan, sqrt, etc.), Magnitude, Distance, DotProduct, Normalize, or just rely on Autosteer(goal) / Autothrottle(goal, speed) for car driving |
2D thinking: pos[0], pos[1], heading in degrees |
Everything is 3D Vector3. Access components via vec.x, vec.y, vec.z. There is no scalar "heading". Use Autosteer for car aim. |
transform.Position / transform.position on a Transform node |
RelativePosition(transform_node, "Self") returns the world Vector3 |
Self.Position / Ball.Position / entity.Velocity / Self.TeamSpawn / Game.DeltaTime — dotted accessors on a game entity |
Use the simulation-prefixed node helpers everywhere. Volleyball: VolleyballGetVector3("Self Position"), VolleyballGetVector3("Ball Velocity"), VolleyballGetTransform("Self Team Spawn"), VolleyballGetBool("Self Can Jump"), VolleyballGetFloat("Delta time"). Other sims have their own helpers (DemoDerbyGetTransform, DemoDerbyGetCar, CarGetPart(...).PartTransform, SurvivalGetTransform, ParkingGetTransform, etc.). Never assume .Position / .Velocity / .Transform exists on a Node — it does not, and examples that used to show that shortcut have been rewritten. |
GetTransform(...) / GetBool(...) / GetFloat(...) / GetVector3(...) used in Survival / Parking / Demo Derby / Soccer / RacingV2 graphs |
Those unprefixed names are Volleyball-only backward-compat aliases. In other sims you'll silently build the wrong Unity node. Use the sim prefix: SurvivalGetTransform / ParkingGetTransform / DemoDerbyGetTransform / SoccerGetTransform / RacingV2GetFloat, etc. |
Treating CreateFunction like Region, or like Python customNodes.py |
Custom Functions change execution: body nodes only run via CustomFunction(...) calls; params/optional return are the interface. Region is visual-only. |
Building Power / math inside CreateFunction but forgetting the return wire |
Always call SetFunctionReturn(fn, powered) so Power.Float1 connects to CreateFunction Return (Any1 In). Without it, CustomFunction(...) output is null. |
Wiring Return to port id Any |
Return is Any1 polarity In (not Any). Param1 Out is also Any1 — polarity distinguishes them. |
One SlimeController / single controller for Soccer |
Soccer needs SoccerController(1..4, moveTo, sprint, interact) per player on the team graph |
Magnitude(CarInfo(car).Velocity), ClampFloat(CarInfo(car).Health, ...), pos + CarInfo(car).Velocity * dt, ConditionalSetFloat(CarGetPart(3, car).HealthPercent < 50, ...) |
Broken in the current library. Only .CarTransform (on CarInfo) and .PartTransform (on CarGetPart) can be passed to another node — everything else raises KeyError: 'Vector32' / 'Bool3' / 'Bool4' / 'Float5' / 'Float6' / 'Float2' in ConnectPorts. Plan your bot around Autosteer / Autothrottle + DemoDerbyGetCar / CarGetPart(3, ...).PartTransform + raycast sensors. See Multi-output component accessor bug. |
Forget to call SaveData(...) at the end |
Always finish with SaveData("YourBotName", "auto") — without this the script does literally nothing |
Wrong (looks like a runtime loop, none of this works):
import AIGamePyLibrary as aig # treats helpers as methods on a sim object
sim = aig.InitializeDemoDerby(name="Gemini", country="USA",
modifier_llm=True, save_file="Gemini") # not real kwargs
while sim.is_active(): # no such method
me = sim.get_self_data() # no such method
for op in sim.get_opponents(): # no such method
if dist(me, op) < 50: # plain Python if won't compile to a node
sim.set_controls(throttle=1.0, steer=0.2) # no such method
sim.update() # no such methodRight (build a graph, mark it LLM-driven, save it):
from AIGamePyLibrary import *
props = InitializeDemoDerby(
"Gemini", "Gemini", "Tan", 0, 2, "Brown", 0, "Blue", "",
)
props.data["modifier"] = "True" # mark as LLM-driven (see "Marking your bot as LLM-driven")
self_pos = RelativePosition(DemoDerbyGetTransform(0), "Self")
target = DemoDerbyGetCar(8) # nearest active car
goal = RelativePosition(CarGetPart(3, target).PartTransform, "Self")
ModularUniformController(Autothrottle(goal, 25.0), Autosteer(goal), Float(0.0))
SaveData("Gemini", "auto")For a slightly fuller derby starter (with sensors and a panic brake) see the Minimal complete Demo Derby example. Grok.py now demonstrates a safe, aggressive ramming strategy with sensor-based unsticking that avoids the KeyError: 'Bool4' (see the new warning at the top of Notes for LLM Authors).
This is a known library bug, not an LLM mistake. It's documented here so LLM authors stop writing scripts that hit it.
Any of these will raise a KeyError inside AIGamePyLibrary/lib.py → ConnectPorts at compile time (when SaveData(...) runs, or earlier if the consuming node is constructed first):
# --- all of these crash ---
self_info = CarInfo(DemoDerbyGetCar(2))
self_speed = Magnitude(self_info.Velocity) # KeyError: 'Vector32'
is_alive = self_info.Health > 0 # KeyError: 'Float5'
if_imm = ConditionalSetFloat(self_info.IsImmobile, -1.0, 1.0) # KeyError: 'Bool4'
rank_ok = self_info.Rank < 3 # KeyError: 'Float6'
# unpacking doesn't save you — the unpacked Nodes are the same broken objects:
car_tf, velocity, is_ai, is_immobile, health, rank = CarInfo(car)
Magnitude(velocity) # still KeyError: 'Vector32'
# GetCarPart has the same shape of bug:
part = CarGetPart(3, target)
weak = part.HealthPercent < 30 # KeyError: 'Float2'CarInfoComponents and GetCarPartComponents store each accessor's outputIndex as its global position in the node's port list, but ConnectPorts builds the Unity port name as f"{inputType}{outputIndex}" — which must be the type-local port number. Unity's CarInfo node publishes Transform1, Vector31, Bool1, Bool2, Float1, Float2 (type-local), but the Python wrappers ask for Transform1, Vector32, Bool3, Bool4, Float5, Float6. Only the first port of each multi-output node (Transform1, position 1) lines up by accident.
For reference, here's what each accessor currently does and what port id it asks for:
| Accessor | outputIndex |
Port name built | Actual Unity port | Status |
|---|---|---|---|---|
CarInfo(...).CarTransform |
1 | Transform1 |
Transform1 |
✅ works |
CarInfo(...).Velocity |
2 | Vector32 |
Vector31 |
❌ KeyError |
CarInfo(...).IsAI |
3 | Bool3 |
Bool1 |
❌ KeyError |
CarInfo(...).IsImmobile |
4 | Bool4 |
Bool2 |
❌ KeyError |
CarInfo(...).Health |
5 | Float5 |
Float1 |
❌ KeyError |
CarInfo(...).Rank |
6 | Float6 |
Float2 |
❌ KeyError |
CarGetPart(...).PartTransform |
1 | Transform1 |
Transform1 |
✅ works |
CarGetPart(...).HealthPercent |
2 | Float2 |
Float1 |
❌ KeyError |
HitInfoComponents sidesteps this by storing outputIndex=1 for both WasHit and Distance and instead setting node.type to bool / float so the consumer prefixes the right port type — CarInfoComponents and GetCarPartComponents would need the same kind of fix.
LLM authors: plan your bot so you never pass the broken accessors into another node. In practice that's easier than it sounds because the library already gives you graph-side alternatives:
- Don't velocity-lead the aim. Skip
target_pos + target_vel * dt. LetAutosteer(goal)andAutothrottle(goal, speed)handle aim + speed management — they already compensate for relative motion and obstacles internally. - Don't check self-speed via
Magnitude(CarInfo(...).Velocity). For stuck-recovery, gate onCarInfo(self).IsImmobile… wait, that's broken too — useSpherecast+CarRaycasts+HitInfoto detect "nose blocked" instead, and flip to reverse on that signal. - Don't branch on opponent
HealthorRankfromCarInfo. Use the built-in target selectors that already bake those in:DemoDerbyGetCar(5)(lowest health),DemoDerbyGetCar(8)(nearest active),DemoDerbyGetCar(12)(nearest with disabled steering),DemoDerbyGetCar(20)(highest ranked),DemoDerbyGetCar(21)(lowest ranked), etc. Pick the target by mode, then aim atCarGetPart(3, target).PartTransform(nearest crucial part). - Don't branch on
CarGetPart(...).HealthPercent.CarGetPartmode2is "weakest part" — use that part'sPartTransformdirectly and skip the health comparison. - Do keep using
.CarTransformand.PartTransform. Both resolve toTransform1(index 1) and compile correctly — pipe them throughRelativePosition(..., "Self")to get a worldVector3.
If you find yourself reaching for one of the broken accessors, swap in the pattern on the right instead. Every example on the right compiles against the current library.
Broken pattern (crashes in ConnectPorts) |
Safe replacement |
|---|---|
speed = Magnitude(CarInfo(self).Velocity) — reason about own speed |
Skip it. Autothrottle(goal, desired_speed) already manages cruise speed for you. If you absolutely need a "too slow" signal, use HitInfo(ray_forward) + HitInfo(ray_back) proximity — if the nose is pinned and the rear is clear, you're stuck. |
lead = target_pos + CarInfo(target).Velocity * dt — predict the target |
Skip it. Autosteer(goal) already tracks a moving Vector3 goal reasonably well. |
is_stuck = CarInfo(self).IsImmobile (then ConditionalSetFloat(is_stuck, -1.0, throttle_fwd)) |
front_hit, front_dist = HitInfo(ray_forward)rear_hit, rear_dist = HitInfo(ray_back)nose_wedged = front_hit & (front_dist < 2.0) & (goal_dist > 6.0)rear_clear = ~rear_hit | (rear_dist > 4.0)reverse_mode = nose_wedged & rear_clear |
finish_them = CarInfo(target).Health < 50 |
Pre-select a weak target instead of branching on health. target = DemoDerbyGetCar(5) (lowest health) or DemoDerbyGetCar(12) (nearest with disabled steering). |
winning = CarInfo(self).Rank < 3 |
No safe replacement right now. Drop the rank-based branching — the built-in target selectors already keep you aggressive. |
is_ai = CarInfo(target).IsAI (avoid human targets) |
No safe replacement right now. Drop the filter — the derby is set up to only pit eligible cars against each other. |
weak = CarGetPart(3, target).HealthPercent < 30 |
target_part = CarGetPart(2, target) # mode 2 = weakest partgoal = RelativePosition(target_part.PartTransform, "Self") |
This is the exact pattern that crashed the reference claude.py with KeyError: 'Bool4'. Here's the broken version (do not ship) and the raycast-only version (compiles, does the same job):
# ❌ CRASHES at SaveData time with KeyError: 'Bool4'
self_info = CarInfo(DemoDerbyGetCar(2))
is_stuck = self_info.IsImmobile # outputIndex=4 -> asks for Bool4
throttle_fwd = Autothrottle(goal, 28.0)
throttle = ConditionalSetFloat(is_stuck, -1.0, throttle_fwd) # boom# ✅ Same intent, raycast-only, compiles today
sensor = Spherecast(1.2, 12.0)
ray_fl, ray_f, ray_fr, ray_l, ray_r, ray_bl, ray_b, ray_br = CarRaycasts(sensor)
front_hit, front_dist = HitInfo(ray_f)
rear_hit, rear_dist = HitInfo(ray_b)
nose_wedged = front_hit & (front_dist < 2.0) & (goal_dist > 6.0)
rear_clear = ~rear_hit | (rear_dist > 4.0)
reverse_mode = nose_wedged & rear_clear
throttle_fwd = Autothrottle(goal, 28.0)
throttle = ConditionalSetFloat(reverse_mode, -1.0, throttle_fwd)Any bot that follows the Minimal complete Demo Derby example pattern (target car → nearest crucial part → Autosteer + Autothrottle + a forward raycast for panic-brake, no CarInfo component reads other than .CarTransform) avoids this bug entirely.