Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 c5d86c8d8b Bots spread out about their objective and keep to it
Each bot has a direction of its own (the golden angle on from the last):
far from a shared goal (the enemy's flag, the ball, the hill) it comes by
a way of its own, out to its side of the straight way, straight only for
the last 10 world units; holding a place it takes a spot of its own: on
the hill, about its flag's stand (every other guard ahead of it towards
the enemy's), about a teammate it escorts. With no way to its spot it
takes the point itself. A bot with an objective no longer leaves it in a
fight for a weapon close by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:55:28 +02:00
Stefan MaierandClaude Opus 5.5 1c8e651bb1 Bots play with their team
In a game of teams a bot holds its fire while a teammate stands in the
line of it (and steps aside for a clear shot), takes on first the enemies
near its teammates (people above all), escorts a teammate carrying the
enemy's flag or the ball a few world units off on its own side, and, with
nothing to do, every other bot keeps near the nearest person on its team.
A goal it finds no way to three times running (a person up where only a
teleporter goes, a thing out of reach) is given up for a while, and a
roaming bot then goes somewhere near at random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:41:08 +02:00
+333 -22
View File
@@ -32,6 +32,12 @@ In the objective games a bot plays for the objective, fighting on the way:
- king of the hill: to the hill, and around in it;
- oddball: to the ball (taken as a weapon is), hunting whoever carries it;
with it, away from the enemy, striking those who come near.
In a game of teams a bot plays with its team: it holds its fire while a
teammate is in the line of it, takes on first the enemies near its
teammates (people above all), escorts a teammate who carries the enemy's
flag or the ball, and, with nothing to do, every other bot keeps near the
nearest person on its team.
*/
#include "cseries.h"
@@ -111,6 +117,12 @@ struct bot
real sidestep;
/* whether it walks to a point this tick (not only weaving in a fight) */
boolean walking;
/* the paths to its goal not found in a row, and goals given up for a
while on that account: following a person, a thing to pick up */
short path_failures;
long follow_again_time;
long ignored_item_index;
long ignored_item_time;
real wander_yaw;
short weave_phase;
/* where it is going, and the way there */
@@ -127,6 +139,8 @@ struct bot
/* the objective, as it played it this tick: where it goes, whom it
hunts first, and whether it carries the flag or ball (no firing) */
boolean has_objective;
/* the objective is a place to hold (the hill, its own flag's stand) */
boolean holding;
real_point3d objective;
long objective_unit_index;
boolean carrying;
@@ -232,6 +246,124 @@ static boolean bot_can_see(
from, &vector, unit_index, &collision);
}
/* ---------- the team */
/* whether a player is a person (on a machine of its own, not a bot's) */
static boolean bot_player_is_person(
struct player_datum const *player)
{
return !network_game_server_is_bot_machine(player->network_player_data.machine_index);
}
/* how much nearer an enemy at this point counts for being near the bot's
teammates: up to 10 world units for one by a person, 6 by a bot */
static real bot_threat_to_team(
struct player_datum const *player,
real_point3d const *enemy)
{
real threat = 0.0f;
short index;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *mate = distributed_player(index);
long unit_index;
real_point3d head;
real distance;
real most;
if (!mate || mate == player || mate->team_index != player->team_index)
continue;
unit_index = distributed_living_unit(mate);
if (unit_index == NONE)
continue;
unit_get_camera_position(unit_index, &head);
distance = distance3d(enemy, &head);
most = bot_player_is_person(mate) ? 10.0f : 6.0f;
if (distance < 12.0f)
threat = MAX(threat, most * (1.0f - distance / 12.0f));
}
return threat;
}
/* whether a teammate stands in the line of fire to a point (nearer than
it, within 0.8 world units of the line) */
static boolean bot_teammate_in_line(
struct player_datum const *player,
real_point3d const *eye,
real_point3d const *target)
{
real_vector3d line;
real length;
short index;
if (!game_engine_has_teams())
return FALSE;
vector_from_points3d(eye, target, &line);
length = magnitude3d(&line);
if (length < 0.01f)
return FALSE;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *mate = distributed_player(index);
long unit_index;
real_point3d center;
real_vector3d offset;
real along;
real_point3d nearest;
if (!mate || mate == player || mate->team_index != player->team_index)
continue;
unit_index = distributed_living_unit(mate);
if (unit_index == NONE)
continue;
unit_get_camera_position(unit_index, &center);
center.z -= 0.25f;
vector_from_points3d(eye, &center, &offset);
along = dot_product3d(&offset, &line) / length;
if (along <= 0.0f || along >= length)
continue;
nearest.x = eye->x + line.i * along / length;
nearest.y = eye->y + line.j * along / length;
nearest.z = eye->z + line.k * along / length;
if (distance3d(&nearest, &center) < 0.8f)
return TRUE;
}
return FALSE;
}
/* the nearest person on the bot's team, alive (NONE: none) */
static long bot_nearest_person(
struct player_datum const *player,
real_point3d const *position)
{
long best = NONE;
real best_distance = 40.0f;
short index;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *mate = distributed_player(index);
long unit_index;
real_point3d origin;
real distance;
if (!mate || mate == player || mate->team_index != player->team_index || !bot_player_is_person(mate))
continue;
unit_index = distributed_living_unit(mate);
if (unit_index == NONE)
continue;
object_get_origin(unit_index, &origin);
distance = distance3d(position, &origin);
if (distance < best_distance)
{
best_distance = distance;
best = unit_index;
}
}
return best;
}
/* the nearest enemy unit the bot can see, or NONE */
static long bot_find_target(
struct player_datum const *player,
@@ -256,6 +388,11 @@ static long bot_find_target(
continue;
unit_get_camera_position(unit_index, &head);
distance = distance3d(eye, &head);
if (distance >= bots.sight_range)
continue;
/* (one near a teammate counts as nearer: helped first) */
if (teams)
distance -= bot_threat_to_team(player, &head);
if (distance < best_distance && bot_can_see(player->unit_index, eye, &head))
{
best = unit_index;
@@ -299,6 +436,7 @@ static boolean bot_find_path(
if (goal_surface == NONE)
{
bots.fail_goal++;
bot->path_failures++;
return FALSE;
}
/* (a little wider than a biped: paths that keep off walls and corners) */
@@ -333,9 +471,11 @@ static boolean bot_find_path(
bot->path.valid = FALSE;
bots.paths_failed++;
bots.fail_search++;
bot->path_failures++;
return FALSE;
}
bots.paths_found += bot->path.valid != FALSE;
bot->path_failures = 0;
return bot->path.valid;
}
@@ -776,6 +916,88 @@ static boolean bots_ctf_base(
return FALSE;
}
/* near a teammate who carries something, a few world units off on the bot's
side of it (an escort, not on the carrier's heels) */
static void bot_escort_point(
real_point3d const *position,
real_point3d const *carried,
real_point3d *point)
{
real dx = position->x - carried->x;
real dy = position->y - carried->y;
real length = (real)sqrt(dx * dx + dy * dy);
*point = *carried;
if (length > 0.01f)
{
point->x += dx / length * 2.5f;
point->y += dy / length * 2.5f;
}
}
/* Bots with one place to go spread out: each has a direction of its own
(the golden angle on from the last bot's), a place of its own about a
point to hold, and comes to a point from far off by a way of its own,
wide of the others', going straight to it only for the last stretch. */
static real bot_spread_angle(
long player_index)
{
return (real)DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) * 2.39996f;
}
/* the bot's place about a point (radius: world units) */
static void bot_spot_around(
real_point3d const *center,
long player_index,
real radius,
real_point3d *spot)
{
real angle = bot_spread_angle(player_index);
*spot = *center;
spot->x += (real)cos(angle) * radius;
spot->y += (real)sin(angle) * radius;
}
/* its place about a point, or the point itself when there is no way to the
place (in a wall, past a ledge) */
static void bot_hold_spot(
struct bot const *bot,
real_point3d const *center,
real radius,
real_point3d *spot)
{
if (bot->path_failures >= 3)
*spot = *center;
else
bot_spot_around(center, bot->player_index, radius, spot);
}
/* where it heads for a point from where it is: from far off, out to a side
of the straight way (its own side and width), in from 10 world units */
static void bot_approach(
struct bot const *bot,
real_point3d const *position,
real_point3d const *goal,
real_point3d *point)
{
long index = DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index);
real dx = goal->x - position->x;
real dy = goal->y - position->y;
real distance = (real)sqrt(dx * dx + dy * dy);
real side = (index & 1 ? 1.0f : -1.0f) * (3.0f + (real)((index >> 1) % 3) * 1.5f);
real share;
*point = *goal;
/* (no way to the side: straight) */
if (distance < 10.0f || bot->path_failures >= 2)
return;
share = MIN((distance - 10.0f) / 10.0f, 1.0f);
point->x += -dy / distance * side * share;
point->y += dx / distance * side * share;
}
/* what the objective asks of the bot now (bot->has_objective and the rest) */
static void bot_play_objective_now(
struct bot *bot,
@@ -809,6 +1031,7 @@ static void bot_play_objective_now(
short index;
bot->has_objective = FALSE;
bot->holding = FALSE;
bot->objective_unit_index = NONE;
bot->carrying = FALSE;
if (type == game_engine_king)
@@ -823,13 +1046,12 @@ static void bot_play_objective_now(
dx = center.x - position->x;
dy = center.y - position->y;
bot->has_objective = TRUE;
bot->objective = center;
/* (on the hill: about in it, not on its middle) */
if (dx * dx + dy * dy < 1.5f * 1.5f)
{
bot->objective.x += (real)cos(bot->weave_phase * 0.05f) * 1.0f;
bot->objective.y += (real)sin(bot->weave_phase * 0.05f) * 1.0f;
}
bot->holding = TRUE;
/* (near: a place of its own on the hill; far: a way of its own) */
if (dx * dx + dy * dy < 6.0f * 6.0f)
bot_hold_spot(bot, &center, 1.2f, &bot->objective);
else
bot_approach(bot, position, &center, &bot->objective);
}
return;
}
@@ -876,23 +1098,42 @@ static void bot_play_objective_now(
bot->objective = own->position;
return;
}
/* (every third guards its own) */
/* (every third guards its own: about the stand, or, every other one
of them, ahead of it towards the enemy's) */
if ((DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) % 3) == 2 && bots_ctf_base(player->team_index, &base))
{
real_point3d enemy_base;
bot->has_objective = TRUE;
bot->objective = base;
if (distance3d(position, &base) < 4.0f)
bot->holding = TRUE;
if (((DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) / 3) & 1) &&
bots_ctf_base((player->team_index + 1) % 2, &enemy_base))
{
bot->objective.x += (real)cos(bot->weave_phase * 0.03f) * 3.0f;
bot->objective.y += (real)sin(bot->weave_phase * 0.03f) * 3.0f;
real dx = enemy_base.x - base.x;
real dy = enemy_base.y - base.y;
real length = (real)sqrt(dx * dx + dy * dy);
bot->objective = base;
if (length > 0.01f)
{
bot->objective.x += dx / length * 7.0f;
bot->objective.y += dy / length * 7.0f;
}
}
else
{
bot_hold_spot(bot, &base, 3.0f, &bot->objective);
}
return;
}
/* theirs: to take it, or to cover its carrier */
/* theirs: to take it, or to escort the teammate carrying it */
if (enemy)
{
bot->has_objective = TRUE;
bot->objective = enemy->position;
if (enemy->carrier_player_index != NONE)
bot_hold_spot(bot, &enemy->position, 3.0f, &bot->objective);
else
bot_approach(bot, position, &enemy->position, &bot->objective);
}
return;
}
@@ -911,13 +1152,20 @@ static void bot_play_objective_now(
bot->carrying = TRUE;
return;
}
/* (one a teammate carries: not to be chased) */
/* (one a teammate carries: escorted, not chased) */
if (item->carrier_player_index != NONE && game_engine_has_teams())
{
struct player_datum *carrier = player_try_and_get(item->carrier_player_index);
if (carrier && carrier->team_index == player->team_index)
{
if (!nearest)
{
bot->has_objective = TRUE;
bot_hold_spot(bot, &item->position, 3.0f, &bot->objective);
}
continue;
}
}
distance = distance3d(position, &item->position);
if (distance < nearest_distance)
@@ -929,7 +1177,7 @@ static void bot_play_objective_now(
if (nearest)
{
bot->has_objective = TRUE;
bot->objective = nearest->position;
bot_approach(bot, position, &nearest->position, &bot->objective);
if (nearest->carrier_player_index != NONE)
{
struct player_datum *carrier = player_try_and_get(nearest->carrier_player_index);
@@ -976,6 +1224,7 @@ static void bot_think(
real wanted_yaw;
real wanted_pitch = 0.0f;
boolean firing = FALSE;
boolean following = FALSE;
real moved;
csmemset(action, 0, sizeof(*action));
@@ -1097,13 +1346,13 @@ static void bot_think(
}
bot->weave_phase++;
/* (a good weapon or powerup close by: taken in the fight too) */
if (!bot->carrying && (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS / 2 ||
if (!bot->carrying && !bot->has_objective && (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS / 2 ||
game_time_get() < bot->item_look_time))
{
bot->item_look_time = game_time_get();
bot->item_index = bot_find_item(unit, &position, 10.0f, 4.0f);
}
if (bot->item_index != NONE && !bot->carrying && object_try_and_get(bot->item_index) &&
if (bot->item_index != NONE && !bot->carrying && !bot->has_objective && object_try_and_get(bot->item_index) &&
bot_item_worth(unit, bot->item_index) > 0.0f)
{
real_point3d item_position;
@@ -1153,6 +1402,13 @@ static void bot_think(
firing = !bot->carrying && distance < BOT_FIRE_RANGE &&
fabsf(bot_angle_difference(wanted_yaw, bot->yaw)) < BOT_FIRE_CONE &&
fabsf(wanted_pitch - bot->pitch) < BOT_FIRE_CONE;
/* (a teammate in the line of fire: held, and a step aside for a
clear shot) */
if (firing && bot_teammate_in_line(player, &eye, &head))
{
firing = FALSE;
action->throttle.j = (bot->weave_phase / 30) & 1 ? 1.0f : -1.0f;
}
/* (a melee blow when it is upon it) */
if (distance < 1.0f)
SET_FLAG(action->control_flags, _unit_control_use_equipment_bit, TRUE);
@@ -1201,6 +1457,8 @@ static void bot_think(
long item = bot_find_item(unit, &position, BOT_ITEM_RANGE, 0.0f);
bot->item_look_time = game_time_get();
if (item == bot->ignored_item_index && game_time_get() - bot->ignored_item_time < 600)
item = NONE;
if (item != bot->item_index)
{
bot->item_index = item;
@@ -1215,8 +1473,16 @@ static void bot_think(
}
if (bot->item_index != NONE && !bot->has_objective)
{
/* (taken or gone: on) */
if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
/* (taken or gone, or no way to it: on) */
if (bot->path_failures >= 3)
{
bot->ignored_item_index = bot->item_index;
bot->ignored_item_time = game_time_get();
bot->path_failures = 0;
bot->item_index = NONE;
bot->has_goal = FALSE;
}
else if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
{
bot->item_index = NONE;
bot->has_goal = FALSE;
@@ -1229,12 +1495,56 @@ static void bot_think(
bot_take_item(bot, player, unit, action);
}
}
/* (in a team, every other bot with nothing to do keeps near the
nearest person on its team) */
following = FALSE;
if (bot->item_index == NONE && !bot->has_objective && game_engine_has_teams() &&
(DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) & 1) == 0 &&
game_time_get() >= bot->follow_again_time)
{
long person = bot_nearest_person(player, &position);
if (person != NONE)
{
real_point3d origin;
following = TRUE;
object_get_origin(person, &origin);
/* (near: where it is, not on top of them) */
if (distance3d(&position, &origin) > 6.0f)
bot_escort_point(&position, &origin, &bot->goal);
else
bot->goal = position;
bot->has_goal = TRUE;
bot->goal_time = game_time_get();
/* (no way to them, as up where only a teleporter goes: not for
ten seconds) */
if (bot->path_failures >= 3)
{
bot->follow_again_time = game_time_get() + 300;
bot->path_failures = 0;
following = FALSE;
}
}
}
/* hunts, or roams: along its way, looking where it goes */
if (bot->item_index == NONE && !bot->has_objective &&
(!bot->has_goal || game_time_get() - bot->goal_time > 90 ||
if (bot->item_index == NONE && !bot->has_objective && !following &&
(!bot->has_goal || game_time_get() - bot->goal_time > 90 || bot->path_failures >= 3 ||
distance3d(&position, &bot->goal) < BOT_GOAL_RADIUS))
{
boolean unreachable = bot->path_failures >= 3;
bot_choose_goal(bot, player, &position);
/* (no way to the last: somewhere near at random instead) */
if (unreachable)
{
real angle = bot_random() * 2.0f * _pi;
bot->goal = position;
bot->goal.x += (real)cos(angle) * 6.0f;
bot->goal.y += (real)sin(angle) * 6.0f;
}
bot->path_failures = 0;
}
wanted_yaw = bot->wander_yaw;
if (bot_next_point(bot, player->unit_index, &position, &point))
@@ -1363,6 +1673,7 @@ static struct bot *bot_get(
free_bot->player_index = player_index;
free_bot->target_unit_index = NONE;
free_bot->item_index = NONE;
free_bot->ignored_item_index = NONE;
}
return free_bot;
}