battle/battle_tick.py
import math
import random
import django
from django.db import transaction
from battle.models import Battle, BattleCharacterInTurn, BattleUnitInTurn, BattleContuberniumInTurn, \
BattleSoldierInTurn, Coordinates, Order
from context_managers import perf_timer
from finem_imperii.app_settings import BATTlE_TICKS_PER_TURN
from unit.models import WorldUnit
def create_next_turn(battle: Battle):
new_turn = battle.get_latest_turn()
new_turn.id = None
new_turn.num += 1
new_turn.save()
prev_turn = battle.battleturn_set.get(num=new_turn.num - 1)
for side in battle.battleside_set.all():
for organization in side.battleorganization_set.all():
for character in organization.battlecharacter_set.all():
bcit = BattleCharacterInTurn.objects.get(
battle_character=character,
battle_turn=prev_turn
)
bcit.id = None
bcit.battle_turn = new_turn
bcit.save()
for unit in organization.battleunit_set.all():
try:
buit = BattleUnitInTurn.objects.get(
battle_unit=unit,
battle_turn=prev_turn
)
if not buit.battlecontuberniuminturn_set.filter(
x_pos__gte=-50, x_pos__lte=50,
z_pos__gte=-50, z_pos__lte=50,
).exists():
buit.battle_unit.in_battle = False
buit.battle_unit.save()
buit.battle_unit.world_unit.status = \
WorldUnit.REGROUPING
buit.battle_unit.world_unit.save()
continue
if not BattleSoldierInTurn.objects.filter(
battle_contubernium_in_turn__battle_unit_in_turn=buit,
wound_status__lt=BattleSoldierInTurn.DEAD
).exists():
buit.battle_unit.in_battle = False
buit.battle_unit.save()
buit.battle_unit.world_unit.status = \
WorldUnit.REGROUPING
buit.battle_unit.world_unit.save()
continue
buit.id = None
buit.battle_turn = new_turn
buit.battle_character_in_turn = BattleCharacterInTurn.objects.get(
battle_turn=new_turn,
battle_character=buit.battle_character_in_turn.battle_character
) if buit.battle_character_in_turn is not None else None
buit.order = buit.battle_unit.get_order()
buit.save()
except BattleUnitInTurn.DoesNotExist:
continue
for contubernium in unit.battlecontubernium_set.all():
try:
bcontubit = BattleContuberniumInTurn.objects.get(
battle_contubernium=contubernium,
battle_turn=prev_turn
)
if not bcontubit.battlesoldierinturn_set.filter(
wound_status__lt=BattleSoldierInTurn.DEAD
).exists():
continue
if (
not -50 <= bcontubit.x_pos <= 50 or
not -50 <= bcontubit.z_pos <= 50
):
continue
bcontubit.id = None
bcontubit.moved_this_turn = False
bcontubit.desires_pos = False
bcontubit.battle_turn = new_turn
bcontubit.battle_unit_in_turn = buit
bcontubit.attack_type_this_turn = None
bcontubit.contubernium_attacked_this_turn = None
bcontubit.save()
except BattleContuberniumInTurn.DoesNotExist:
pass
for soldier in contubernium.battlesoldier_set.all():
try:
bsit = BattleSoldierInTurn.objects.get(
battle_turn=prev_turn,
battle_soldier=soldier
)
if bsit.wound_status == BattleSoldierInTurn.DEAD:
continue
bsit.id = None
bsit.battle_turn = new_turn
bsit.battle_contubernium_in_turn = bcontubit
bsit.save()
except BattleSoldierInTurn.DoesNotExist:
pass
def optimistic_move_desire_resolving(battle: Battle):
while BattleContuberniumInTurn.objects.filter(desires_pos=True, battle_turn=battle.get_latest_turn()).exists():
bcuit = BattleContuberniumInTurn.objects.filter(desires_pos=True, battle_turn=battle.get_latest_turn())[0]
contubernia_desiring_position = battle.get_latest_turn().get_contubernia_desiring_position(bcuit.desired_coordinates())
desired_position_occupier = battle.get_latest_turn().get_contubernium_in_position(bcuit.desired_coordinates())
if desired_position_occupier:
# test if mutually desiring positions
if desired_position_occupier.desires_pos:
for desirer in contubernia_desiring_position:
if desirer.coordinates() == desired_position_occupier.desired_coordinates():
grant_position_swap(desirer, desired_position_occupier)
continue
# TODO: try to move blocking contubernium before giving up
contubernia_desiring_position.update(desires_pos=False)
else:
contubernia_desiring_position.update(desires_pos=False)
else:
desire_getter = get_highest_priority_desire(contubernia_desiring_position)
grant_position_desire(desire_getter)
contubernia_desiring_position.update(desires_pos=False)
def grant_position_swap(contubernium1: BattleContuberniumInTurn, contubernium2: BattleContuberniumInTurn):
contubernium1.x_pos = 31337
contubernium1.save()
grant_position_desire(contubernium2)
grant_position_desire(contubernium1)
def grant_position_desire(desire_getter: BattleContuberniumInTurn):
desire_getter.desires_pos = False
desire_getter.moved_this_turn = True
desire_getter.x_pos = desire_getter.desired_x_pos
desire_getter.z_pos = desire_getter.desired_z_pos
desire_getter.save()
def get_highest_priority_desire(contubernium_list: list) -> BattleContuberniumInTurn:
highest_prio = -1
result = None
for contubernium in contubernium_list:
order = contubernium.battle_unit_in_turn.order
what = order.what if order else Order.STAND
prio = Order.ORDER_PRIORITY[what]
if prio > highest_prio:
result = contubernium
highest_prio = prio
return result
def unit_movement(battle: Battle):
# first pass: desire positions / optimistic move
for battle_unit_in_turn in BattleUnitInTurn.objects.filter(battle_turn=battle.get_latest_turn()):
for battle_contubernium_in_turn in battle_unit_in_turn.battlecontuberniuminturn_set.all():
target_distance_function = get_target_distance_function(battle_contubernium_in_turn)
if target_distance_function:
optimistic_move_desire_formulation(battle_contubernium_in_turn, target_distance_function)
optimistic_move_desire_resolving(battle)
# second pass: if could not move, do "safe" move
for battle_unit_in_turn in BattleUnitInTurn.objects.filter(battle_turn=battle.get_latest_turn()):
for battle_contubernium_in_turn in battle_unit_in_turn.battlecontuberniuminturn_set.filter(moved_this_turn=False):
target_distance_function = get_target_distance_function(battle_contubernium_in_turn)
if target_distance_function:
safe_move(battle_contubernium_in_turn, target_distance_function)
# finalize
for battle_unit_in_turn in BattleUnitInTurn.objects.filter(battle_turn=battle.get_latest_turn()):
battle_unit_in_turn.update_pos()
check_if_order_done(battle_unit_in_turn)
def check_if_order_done(battle_unit_in_turn: BattleUnitInTurn):
order = battle_unit_in_turn.battle_unit.get_order()
if order:
if order.what == Order.MOVE:
if euclidean_distance(battle_unit_in_turn.coordinates(), order.target_location_coordinates()) == 0:
order.done = True
order.save()
def closest_in_set(coords, contubernium_set):
closest = None
distance = None
for contubernium in contubernium_set:
tentative_distance = euclidean_distance(
coords, contubernium.coordinates())
if closest is None or tentative_distance < distance:
closest = contubernium
distance = tentative_distance
return closest, distance
def get_target_distance_function(battle_contubernium_in_turn: BattleContuberniumInTurn):
order = battle_contubernium_in_turn.battle_unit_in_turn.battle_unit.get_order()
enemy_contubernia = BattleContuberniumInTurn.objects.filter(
battle_turn=battle_contubernium_in_turn.battle_turn
).exclude(
battle_contubernium__battle_unit__battle_side=
battle_contubernium_in_turn.battle_contubernium.battle_unit.battle_side
)
if order:
if order.what == Order.STAND or (
order.what == Order.RANGED_AND_STAND and
battle_contubernium_in_turn.ammo_remaining == 0
):
return None
if order.what == Order.MOVE:
unit_target = order.target_location_coordinates()
target = Coordinates(
x=(unit_target.x + battle_contubernium_in_turn.battle_contubernium.x_offset_to_unit),
z=(unit_target.z + battle_contubernium_in_turn.battle_contubernium.z_offset_to_unit)
)
def result(coords: Coordinates):
return euclidean_distance(coords, target)
return result
if order.what == Order.FLEE or (
order.what == Order.RANGED_AND_FLEE and
battle_contubernium_in_turn.ammo_remaining == 0
):
original_enemy_distance = closest_in_set(battle_contubernium_in_turn.coordinates(), enemy_contubernia)[1]
def result(coords: Coordinates):
closest, distance = closest_in_set(coords, enemy_contubernia)
return (original_enemy_distance + 10) - distance if distance is not None else 0
return result
if order.what == Order.CHARGE or (
order.what == Order.RANGED_AND_CHARGE and
battle_contubernium_in_turn.ammo_remaining == 0
):
def result(coords: Coordinates):
closest, distance = closest_in_set(coords, enemy_contubernia)
return distance if distance is not None and distance >= 2 else 0
return result
if order.what == Order.ADVANCE_IN_FORMATION:
z_direction = -1 if battle_contubernium_in_turn.battle_contubernium.battle_unit.battle_side.z else 1
z_offset = battle_contubernium_in_turn.battle_turn.num * z_direction
target = Coordinates(
x=battle_contubernium_in_turn.battle_contubernium.starting_x_pos,
z=battle_contubernium_in_turn.battle_contubernium.starting_z_pos + z_offset
)
def result(coords: Coordinates):
return euclidean_distance(coords, target)
return result
if order.what in (
Order.RANGED_AND_CHARGE,
Order.RANGED_AND_FLEE,
Order.RANGED_AND_STAND
) and battle_contubernium_in_turn.ammo_remaining > 0:
def result(coords: Coordinates):
closest, distance = closest_in_set(coords, enemy_contubernia)
shot_range = battle_contubernium_in_turn.battle_unit_in_turn.battle_unit.world_unit.shot_range()
if distance > shot_range:
return distance - shot_range
if distance < shot_range - 1:
return shot_range - distance
return 0
return result
if order.what == Order.STAND_AND_DISTANCE:
def result(coords: Coordinates):
closest, enemy_distance = closest_in_set(coords, enemy_contubernia)
shot_range = battle_contubernium_in_turn.battle_unit_in_turn.battle_unit.world_unit.shot_range()
if shot_range == 0:
min_enemy_distance = 7
else:
min_enemy_distance = shot_range - 1
if enemy_distance > min_enemy_distance:
return 0
else:
return min_enemy_distance - enemy_distance
return result
def optimistic_move_desire_formulation(battle_contubernium_in_turn: BattleContuberniumInTurn, target_distance_function):
def tile_availability_test(coords: Coordinates):
return True
if target_distance_function(battle_contubernium_in_turn.coordinates()) > 0:
path = find_path(battle_contubernium_in_turn, target_distance_function, tile_availability_test)
if len(path) > 1:
battle_contubernium_in_turn.desires_pos = True
battle_contubernium_in_turn.desired_x_pos = path[1].x
battle_contubernium_in_turn.desired_z_pos = path[1].z
battle_contubernium_in_turn.save()
def safe_move(battle_contubernium_in_turn: BattleContuberniumInTurn, target_distance_function):
turn = battle_contubernium_in_turn.battle_turn
def tile_availability_test(coords: Coordinates):
return True if turn.get_contubernium_in_position(coords) is None else False
if target_distance_function(battle_contubernium_in_turn.coordinates()) > 0:
path = find_path(battle_contubernium_in_turn, target_distance_function, tile_availability_test)
if len(path) > 1:
battle_contubernium_in_turn.moved_this_turn = True
battle_contubernium_in_turn.x_pos = path[1].x
battle_contubernium_in_turn.z_pos = path[1].z
#TODO WARNING: HORRIBLE HACK STARTS HERE
#(to avoid unique constraint errors when contubs overlap for some reason)
with transaction.atomic():
try:
battle_contubernium_in_turn.save()
except django.db.utils.IntegrityError as e:
pass
def euclidean_distance(start: Coordinates, goal: Coordinates):
return math.sqrt((start.x - goal.x)**2 + (start.z - goal.z)**2)
def coordinate_neighbours(coord: Coordinates):
result = []
for dx in (-1, 0, 1):
for dz in (-1, 0, 1):
if not dx == dz == 0:
result.append(Coordinates(coord.x + dx, coord.z + dz))
return result
def find_path(battle_contubernium_in_turn: BattleContuberniumInTurn, target_distance_function, tile_availability_test) -> list:
starting_coordinates = battle_contubernium_in_turn.coordinates()
if target_distance_function(starting_coordinates) <= 0:
return True
closed_set = set()
open_set = set()
open_set.add(starting_coordinates)
came_from = {}
g_score = {}
g_score[starting_coordinates] = 0
f_score = {}
f_score[starting_coordinates] = target_distance_function(starting_coordinates)
while open_set:
minel = None
for el in open_set:
if minel is None or f_score[el] < f_score[minel]:
minel = el
current = minel
open_set.remove(minel)
if target_distance_function(current) <= 0:
# RECONSTRUCT
# print("REACHED GOAL, backtracing")
total_path = [current]
while current in came_from.keys():
current = came_from[current]
if current != starting_coordinates and not tile_availability_test(current):
return []
total_path.append(current)
# print("Backtrace {}".format(current))
total_path.reverse()
return total_path
closed_set.add(current)
for neighbor in coordinate_neighbours(current):
if neighbor in closed_set:
# print("Already closed: {}".format(neighbor))
continue
tentative_g_score = g_score[current] + euclidean_distance(current, neighbor)
if not tile_availability_test(neighbor):
tentative_g_score += 20
# print("Considering {} with score {}".format(neighbor, tentative_g_score))
if neighbor not in open_set:
# print("Adding to open set")
open_set.add(neighbor)
elif tentative_g_score >= g_score[neighbor]:
# print("Better value in g_score map")
continue
# print("Found better path")
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = g_score[neighbor] + target_distance_function(neighbor)
return []
def unit_attack(battle: Battle):
contubernia = list(BattleContuberniumInTurn.objects.filter(
battle_turn=battle.get_latest_turn()
))
random.shuffle(contubernia)
for contubernium in contubernia:
world_unit = contubernium.battle_unit_in_turn.battle_unit.world_unit
enemy_contubernia = BattleContuberniumInTurn.objects.filter(
battle_turn=contubernium.battle_turn
).exclude(
battle_contubernium__battle_unit__battle_side=
contubernium.battle_contubernium.battle_unit.battle_side
)
target_contubernium, distance = \
closest_in_set(contubernium.coordinates(), enemy_contubernia)
if target_contubernium is None:
continue
if distance < 2:
unit_attack_melee(contubernium, target_contubernium)
contubernium.attack_type_this_turn = \
BattleContuberniumInTurn.MELEE_ATTACK
contubernium.contubernium_attacked_this_turn = \
target_contubernium
contubernium.save()
elif (
world_unit.is_ranged() and
distance <= world_unit.shot_range() and
contubernium.ammo_remaining > 0 and
not contubernium.moved_this_turn
):
unit_attack_ranged(contubernium, target_contubernium)
contubernium.attack_type_this_turn = \
BattleContuberniumInTurn.RANGED_ATTACK
contubernium.contubernium_attacked_this_turn = \
target_contubernium
contubernium.save()
def unit_attack_ranged(contubernium: BattleContuberniumInTurn,
target_contubernium: BattleContuberniumInTurn):
for soldier in contubernium.battlesoldierinturn_set.all():
target_soldier = random.choice(
target_contubernium.battlesoldierinturn_set.all()[:]
)
while (random.random() <
0.3 * soldier.attack_chance_multiplier()):
if target_soldier.wound_status < BattleSoldierInTurn.DEAD:
target_soldier.take_hit()
target_soldier.save()
contubernium.ammo_remaining -= 1
if contubernium.ammo_remaining < 0:
contubernium.ammo_remaining = 0
contubernium.save()
def unit_attack_melee(contubernium: BattleContuberniumInTurn,
target_contubernium: BattleContuberniumInTurn):
for soldier in contubernium.battlesoldierinturn_set.all():
target_soldier = random.choice(
target_contubernium.battlesoldierinturn_set.all()[:]
)
while (random.random() <
0.5 * soldier.attack_chance_multiplier()):
if target_soldier.wound_status < BattleSoldierInTurn.DEAD:
target_soldier.take_hit()
target_soldier.save()
def check_end(battle: Battle):
for side in battle.battleside_set.all():
if not BattleSoldierInTurn.objects.filter(
battle_turn=battle.get_latest_turn(),
battle_contubernium_in_turn__battle_contubernium__battle_unit__battle_side=side,
wound_status__lt=BattleSoldierInTurn.DEAD
).exists():
return True
side_0_contubs = BattleContuberniumInTurn.objects.filter(
battle_turn=battle.get_latest_turn(),
battle_contubernium__battle_unit__battle_side__z=False
)
side_1_contubs = BattleContuberniumInTurn.objects.filter(
battle_turn=battle.get_latest_turn(),
battle_contubernium__battle_unit__battle_side__z=False
)
for contub in side_0_contubs:
closest, distance = closest_in_set(
contub.coordinates(), side_1_contubs
)
if distance is not None and distance < 40:
return False
return True
def battle_tick(battle: Battle):
with perf_timer(
"Tick {} for {}".format(battle.get_latest_turn().num, battle)
):
create_next_turn(battle)
unit_movement(battle)
unit_attack(battle)
if check_end(battle):
battle.current = False
battle.save()
def battle_turn(battle: Battle):
for i in range(BATTlE_TICKS_PER_TURN):
battle.refresh_from_db()
if battle.current:
battle_tick(battle)
battle.tile.world.broadcast(
'messaging/messages/battle_progress.html',
'Battle in {}'.format(battle.tile.name),
{'battle': battle},
battle.get_absolute_url()
)