Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ only from `--both-sides`, side/kickoff cells, or `--fuzz-restarts`.
| `press_trigger_flow` | competitive | `counter_flow`, but an all-in press when the ball is lost in our own third. |
| `overload_press` | parked | 4-robot overload/switch attack + 1 block, built to outnumber tiki_taka's shadow line. |
| `high_line_zone` | parked | Zone screen (`BlockShapeTactic`) + switch attack, built to deny tiki_taka 1v1s. |
| `zone_split_shape` | experimental | `split_shape`'s tactics; the 4/1 possession split shifts one robot by the ball's third (3/2 building out from our own third, 2/3 when the ball is lost in theirs), near-ties keep the last split. |

## Latest results

Expand Down
4 changes: 4 additions & 0 deletions utama_core/strategy/kernel_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@
_zone_flow_picker,
build_zone_fluid_kernel_strategy,
)
from utama_core.strategy.zone_split_shape import (
_zone_split_picker,
build_zone_split_shape_kernel_strategy,
)
82 changes: 82 additions & 0 deletions utama_core/strategy/zone_split_shape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""The `zone_split_shape` kernel strategy: `split_shape`'s two tactics with a split that reads the ball's third."""

from __future__ import annotations

from typing import Optional

from utama_core.engine.context import TickContext
from utama_core.engine.strategy import Strategy as KernelSchedulerStrategy
from utama_core.engine.tactic import RobotId
from utama_core.entities.game import Game
from utama_core.motion_planning.src.common.motion_controller import MotionController
from utama_core.strategy.pickers import (
ball_zone,
carrier_first,
friendly_closer_to_ball,
)
from utama_core.tactics.lead_and_support import LeadAndSupportTactic
from utama_core.tactics.shadow_and_mark import ShadowAndMarkTactic


def _attack_count(n: int, edge: Optional[bool], zone: str, prev_attack: Optional[int]) -> int:
"""How many of `n` free robots attack.

With the ball: 4 of 5 in the middle and final thirds, but one fewer in our own third, so a
build-out from the back keeps cover. Without it: 1 of 5 when the ball is in our own or middle
third, but 2 when it is in their final third, so a robot stays up to win the ball back there.
A near-tie (`edge` None) keeps the previous count.
"""
if edge is None:
if prev_attack is not None:
return max(0, min(n, prev_attack))
edge = False
if edge:
count = (n + 1) // 2 + 1
if zone == "own":
count -= 1
else:
count = n // 2 - 1
if zone == "final":
count += 1
return max(0, min(n, count))


def _zone_split_picker(
game: Game,
free_robots: frozenset[RobotId],
prev_partition: Optional[dict[str, frozenset[RobotId]]],
available_tactic_ids: frozenset[str],
) -> dict[str, frozenset[RobotId]]:
"""`split_shape`'s possession split, shifted one robot by where the ball is. See `_attack_count`."""
ordered = carrier_first(game, free_robots)
if not ordered:
return {}
if "attack" not in available_tactic_ids:
return {"defense": frozenset(ordered)} if "defense" in available_tactic_ids else {}
if "defense" not in available_tactic_ids:
return {"attack": frozenset(ordered)}

prev_attack = len(prev_partition["attack"]) if prev_partition and "attack" in prev_partition else None
attack_count = _attack_count(len(ordered), friendly_closer_to_ball(game), ball_zone(game), prev_attack)

partition = {}
if attack_count > 0:
partition["attack"] = frozenset(ordered[:attack_count])
if attack_count < len(ordered):
partition["defense"] = frozenset(ordered[attack_count:])
return partition


def build_zone_split_shape_kernel_strategy(outfield_robot_ids: tuple[int, ...]):
"""`split_shape` with the split shifted by the ball's third of the pitch."""

def _build(motion_controller: MotionController) -> KernelSchedulerStrategy:
ctx = TickContext(motion_controller=motion_controller)
return KernelSchedulerStrategy(
tactics={"attack": LeadAndSupportTactic(), "defense": ShadowAndMarkTactic()},
partitioner=_zone_split_picker,
outfield_robot_ids=outfield_robot_ids,
ctx=ctx,
)

return _build
74 changes: 74 additions & 0 deletions utama_core/tests/strategy/test_zone_split_shape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from types import SimpleNamespace

import pytest

from utama_core.entities.data.object import TeamType
from utama_core.strategy.kernel_strategy import _zone_split_picker
from utama_core.strategy.zone_split_shape import _attack_count

FREE = frozenset({1, 2, 3, 4, 5})
BOTH = frozenset({"attack", "defense"})


@pytest.mark.parametrize(
"edge, zone, expected",
[
(True, "own", 3),
(True, "mid", 4),
(True, "final", 4),
(False, "own", 1),
(False, "mid", 1),
(False, "final", 2),
],
)
def test_five_robot_split_by_possession_and_third(edge, zone, expected):
assert _attack_count(5, edge, zone, prev_attack=None) == expected


def test_a_near_tie_keeps_the_previous_count_whatever_the_third():
assert _attack_count(5, None, "own", prev_attack=4) == 4
assert _attack_count(5, None, "final", prev_attack=1) == 1


def test_a_near_tie_with_no_history_is_the_defensive_split():
assert _attack_count(5, None, "mid", prev_attack=None) == 1


def test_counts_stay_within_the_robots_available():
assert _attack_count(1, True, "mid", None) == 1
assert _attack_count(2, False, "final", None) == 1
assert _attack_count(0, True, "mid", None) == 0


def _game(ball_x, friendly_dist, enemy_dist, carrier=None):
distances = {TeamType.FRIENDLY: (3, friendly_dist), TeamType.ENEMY: (1, enemy_dist)}
return SimpleNamespace(
proximity_lookup=SimpleNamespace(closest_to_ball=lambda team_type_filter: distances[team_type_filter]),
my_team_is_right=True,
field=SimpleNamespace(half_length=4.5),
ball=SimpleNamespace(
p=SimpleNamespace(to_2d=lambda: SimpleNamespace(x=ball_x)), v=SimpleNamespace(x=1.0, y=0.0)
),
friendly_robots={rid: SimpleNamespace(has_ball=rid == carrier) for rid in range(1, 6)},
enemy_robots={1: SimpleNamespace(has_ball=False)},
)


def test_building_out_from_our_own_third_keeps_a_second_defender_back():
# my_team_is_right: our goal is at +x, so x = +3.2 is our own third.
partition = _zone_split_picker(_game(3.2, 0.2, 0.8, carrier=3), FREE, None, BOTH)
assert len(partition["attack"]) == 3 and len(partition["defense"]) == 2
assert 3 in partition["attack"]


def test_losing_the_ball_in_their_third_keeps_a_robot_up():
partition = _zone_split_picker(_game(-3.2, 0.8, 0.2), FREE, None, BOTH)
assert len(partition["attack"]) == 2 and len(partition["defense"]) == 3


def test_a_pinned_slot_gets_every_free_robot():
only_defense = _zone_split_picker(_game(0.0, 0.2, 0.8), FREE, None, frozenset({"defense"}))
assert only_defense == {"defense": FREE}
only_attack = _zone_split_picker(_game(0.0, 0.2, 0.8), FREE, None, frozenset({"attack"}))
assert only_attack == {"attack": FREE}
assert _zone_split_picker(_game(0.0, 0.2, 0.8), FREE, None, frozenset()) == {}
Loading