update UltimateBattleSnake to 4.5.0
This commit is contained in:
@@ -10,7 +10,7 @@ from server.dataset.RLBootstrapDataset import RLBootstrapDataset
|
|||||||
|
|
||||||
class UltimateBattleSnake(TemplateSnake):
|
class UltimateBattleSnake(TemplateSnake):
|
||||||
"""
|
"""
|
||||||
UltimateBattleSnake v4.4.0
|
UltimateBattleSnake v4.5.0
|
||||||
|
|
||||||
All improvements over BestBattleSnake:
|
All improvements over BestBattleSnake:
|
||||||
v3: #1+#9 Simultaneous minimax (both snakes move at once) with hazard/health tracking
|
v3: #1+#9 Simultaneous minimax (both snakes move at once) with hazard/health tracking
|
||||||
@@ -50,9 +50,12 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
v4.4 D1 _minimax_sim: hazard spawn-immunity via previous_hazard_set (no damage on newly-spawned hazard)
|
v4.4 D1 _minimax_sim: hazard spawn-immunity via previous_hazard_set (no damage on newly-spawned hazard)
|
||||||
v4.4 D2 _hazard_will_kill: Dijkstra with per-tile stack cost (was constant entry_stack for whole corridor)
|
v4.4 D2 _hazard_will_kill: Dijkstra with per-tile stack cost (was constant entry_stack for whole corridor)
|
||||||
v4.4 D3 all random.choice fallbacks replaced with deterministic degrade (last_move > center > lexical)
|
v4.4 D3 all random.choice fallbacks replaced with deterministic degrade (last_move > center > lexical)
|
||||||
|
v4.5 E1 _enemy_can_grow_this_turn: health-urgency heuristic + occupied-set check (food blocked = no eat)
|
||||||
|
v4.5 E2 _flood_fill_count: per-turn frozenset-keyed transposition cache; resets each turn
|
||||||
|
v4.5 E3 Snail-specific trail scoring: adjacent hazard density + stack-risk penalty via self._is_snail
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "4.4.0"
|
VERSION = "4.5.0"
|
||||||
Point = tuple[int, int]
|
Point = tuple[int, int]
|
||||||
Coord = dict[str, int]
|
Coord = dict[str, int]
|
||||||
SnakeState = dict[str, Any]
|
SnakeState = dict[str, Any]
|
||||||
@@ -84,6 +87,10 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
self._enemy_dmaps: list[dict] = []
|
self._enemy_dmaps: list[dict] = []
|
||||||
self._enemy_heads: list[tuple[int, int]] = []
|
self._enemy_heads: list[tuple[int, int]] = []
|
||||||
self._base_blocked: set[tuple[int, int]] = set()
|
self._base_blocked: set[tuple[int, int]] = set()
|
||||||
|
self._is_snail: bool = False
|
||||||
|
# E2: per-turn transposition cache for flood-fill (reset each turn)
|
||||||
|
self._bfs_cache: dict[tuple, int] = {}
|
||||||
|
self._bfs_cache_turn: int = -1
|
||||||
# Config
|
# Config
|
||||||
self._planning_depth = max(1, min(4, self._env_int("BATTLE_FUTURE_PLANNING_DEPTH", 2)))
|
self._planning_depth = max(1, min(4, self._env_int("BATTLE_FUTURE_PLANNING_DEPTH", 2)))
|
||||||
self._planning_branch = max(1, min(3, self._env_int("BATTLE_FUTURE_PLANNING_BRANCH", 2)))
|
self._planning_branch = max(1, min(3, self._env_int("BATTLE_FUTURE_PLANNING_BRANCH", 2)))
|
||||||
@@ -161,6 +168,12 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
game_map = game_data.get_map() if hasattr(game_data, "get_map") else None
|
game_map = game_data.get_map() if hasattr(game_data, "get_map") else None
|
||||||
is_constrictor = game_type == "constrictor"
|
is_constrictor = game_type == "constrictor"
|
||||||
is_snail = game_map in {"snail_mode", "snail"} or game_type == "snail_mode"
|
is_snail = game_map in {"snail_mode", "snail"} or game_type == "snail_mode"
|
||||||
|
self._is_snail = is_snail # E3: store for use in _score_move
|
||||||
|
|
||||||
|
# E2: reset per-turn BFS transposition cache
|
||||||
|
if turn != self._bfs_cache_turn:
|
||||||
|
self._bfs_cache = {}
|
||||||
|
self._bfs_cache_turn = turn
|
||||||
|
|
||||||
food_set: set[tuple[int, int]] = {(f["x"], f["y"]) for f in foods}
|
food_set: set[tuple[int, int]] = {(f["x"], f["y"]) for f in foods}
|
||||||
# C1: track hazard stack depth (Snail Mode can stack multiple hazards on one tile)
|
# C1: track hazard stack depth (Snail Mode can stack multiple hazards on one tile)
|
||||||
@@ -178,8 +191,13 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
total_body_cells = len(my_body) + sum(len(s["body"]) for s in other_snakes)
|
total_body_cells = len(my_body) + sum(len(s["body"]) for s in other_snakes)
|
||||||
total_occupancy = total_body_cells / board_area
|
total_occupancy = total_body_cells / board_area
|
||||||
|
|
||||||
|
# E1: build occupied set once so _enemy_can_grow_this_turn can skip food tiles under bodies
|
||||||
|
all_occupied: set[tuple[int, int]] = {(s["x"], s["y"]) for s in my_body}
|
||||||
|
for _s in other_snakes:
|
||||||
|
for _seg in _s["body"]:
|
||||||
|
all_occupied.add((_seg["x"], _seg["y"]))
|
||||||
enemy_can_grow = {
|
enemy_can_grow = {
|
||||||
s["id"]: self._enemy_can_grow_this_turn(s, food_set)
|
s["id"]: self._enemy_can_grow_this_turn(s, food_set, all_occupied)
|
||||||
for s in other_snakes if "id" in s
|
for s in other_snakes if "id" in s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +777,25 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
if hazard_will_kill:
|
if hazard_will_kill:
|
||||||
score -= 10000.0
|
score -= 10000.0
|
||||||
|
|
||||||
|
# E3: Snail Mode trail scoring
|
||||||
|
# Penalise moves that place us in hazard-dense neighbourhoods (future stack risk),
|
||||||
|
# reward moves toward hazard-free space (safer continuation).
|
||||||
|
if self._is_snail and hazard_set:
|
||||||
|
adjacent_hazard_stack = sum(
|
||||||
|
hazard_count.get(n, 1)
|
||||||
|
for n in self._neighbors(point)
|
||||||
|
if n in hazard_set
|
||||||
|
)
|
||||||
|
# Each unit of adjacent total stack costs health faster next turn
|
||||||
|
if adjacent_hazard_stack > 0:
|
||||||
|
score -= adjacent_hazard_stack * 6.0
|
||||||
|
# Bonus for having hazard-free neighbours (escape routes)
|
||||||
|
hazard_free_neighbors = sum(
|
||||||
|
1 for n in self._neighbors(point)
|
||||||
|
if self._in_bounds(n, width, height) and n not in hazard_set and n not in blocked
|
||||||
|
)
|
||||||
|
score += hazard_free_neighbors * 8.0
|
||||||
|
|
||||||
# F12: territory call REMOVED from here — callers (_choose_*_move) apply it after _score_move
|
# F12: territory call REMOVED from here — callers (_choose_*_move) apply it after _score_move
|
||||||
|
|
||||||
score -= self._revisit_penalty(point)
|
score -= self._revisit_penalty(point)
|
||||||
@@ -1385,10 +1422,25 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
def _is_tail_stacked(self, body: list) -> bool:
|
def _is_tail_stacked(self, body: list) -> bool:
|
||||||
return len(body) >= 2 and body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"]
|
return len(body) >= 2 and body[-1]["x"] == body[-2]["x"] and body[-1]["y"] == body[-2]["y"]
|
||||||
|
|
||||||
def _enemy_can_grow_this_turn(self, snake: dict, food_set: set) -> bool:
|
def _enemy_can_grow_this_turn(self, snake:dict, food_set:set, all_occupied:set|None=None) -> bool:
|
||||||
|
"""E1: Estimate if enemy will eat food this turn (tail won't vacate).
|
||||||
|
- Hungry enemies (health < 40) always assumed to eat accessible adjacent food.
|
||||||
|
- Healthy enemies assumed to eat unless the food tile is blocked by a body segment.
|
||||||
|
- all_occupied: full set of body tiles; food under a body can't be eaten this turn.
|
||||||
|
"""
|
||||||
head = snake["head"]
|
head = snake["head"]
|
||||||
|
health = snake.get("health", 100)
|
||||||
for dx, dy in self.DIRECTIONS.values():
|
for dx, dy in self.DIRECTIONS.values():
|
||||||
if (head["x"] + dx, head["y"] + dy) in food_set:
|
pt = (head["x"] + dx, head["y"] + dy)
|
||||||
|
if pt not in food_set:
|
||||||
|
continue
|
||||||
|
# Food blocked by a body segment: snake can't step there, so tail will still vacate
|
||||||
|
if all_occupied is not None and pt in all_occupied:
|
||||||
|
continue
|
||||||
|
# Hungry snakes (health < 40) will eat regardless of other factors
|
||||||
|
if health < 40:
|
||||||
|
return True
|
||||||
|
# Healthy snake with accessible adjacent food: conservative assumption → will eat
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1400,6 +1452,11 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
# ── Pathfinding primitives ────────────────────────────────────────────────────
|
# ── Pathfinding primitives ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _flood_fill_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
def _flood_fill_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||||
|
# E2: transposition cache — frozenset key deduplicates identical blocked sets across branches
|
||||||
|
cache_key = (start, frozenset(blocked))
|
||||||
|
cached = self._bfs_cache.get(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
queue = deque([start])
|
queue = deque([start])
|
||||||
seen = {start}
|
seen = {start}
|
||||||
while queue:
|
while queue:
|
||||||
@@ -1408,7 +1465,9 @@ class UltimateBattleSnake(TemplateSnake):
|
|||||||
if n not in seen and self._in_bounds(n, width, height) and n not in blocked:
|
if n not in seen and self._in_bounds(n, width, height) and n not in blocked:
|
||||||
seen.add(n)
|
seen.add(n)
|
||||||
queue.append(n)
|
queue.append(n)
|
||||||
return len(seen)
|
result = len(seen)
|
||||||
|
self._bfs_cache[cache_key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
def _open_neighbor_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
def _open_neighbor_count(self, start: tuple, blocked: set, width: int, height: int) -> int:
|
||||||
return sum(
|
return sum(
|
||||||
|
|||||||
@@ -745,9 +745,9 @@ class TestVersion(unittest.TestCase):
|
|||||||
|
|
||||||
def test_version_matches_registry(self):
|
def test_version_matches_registry(self):
|
||||||
from snakes import SNAKE_REGISTRY, get_snake_version
|
from snakes import SNAKE_REGISTRY, get_snake_version
|
||||||
self.assertEqual(UltimateBattleSnake.VERSION, "4.4.0")
|
self.assertEqual(UltimateBattleSnake.VERSION, "4.5.0")
|
||||||
self.assertEqual(get_snake_version("UltimateBattleSnake"), "4.4.0")
|
self.assertEqual(get_snake_version("UltimateBattleSnake"), "4.5.0")
|
||||||
self.assertEqual(SNAKE_REGISTRY["UltimateBattleSnake"], "4.4.0")
|
self.assertEqual(SNAKE_REGISTRY["UltimateBattleSnake"], "4.5.0")
|
||||||
|
|
||||||
def test_instance_version_matches_class(self):
|
def test_instance_version_matches_class(self):
|
||||||
snake = UltimateBattleSnake()
|
snake = UltimateBattleSnake()
|
||||||
@@ -1022,5 +1022,193 @@ class TestModeDetection(unittest.TestCase):
|
|||||||
self.assertIn(move, ("up", "down", "left", "right"))
|
self.assertIn(move, ("up", "down", "left", "right"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tests: E1 — probabilistic enemy growth check ─────────────────────────────
|
||||||
|
|
||||||
|
class TestEnemyCanGrow(unittest.TestCase):
|
||||||
|
|
||||||
|
def _enemy(self, head:tuple, body:list, health:int=90) -> dict:
|
||||||
|
return {
|
||||||
|
"id": "e",
|
||||||
|
"head": {"x": head[0], "y": head[1]},
|
||||||
|
"body": [{"x": x, "y": y} for x, y in body],
|
||||||
|
"health": health,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_e1_returns_true_when_adjacent_food_accessible(self):
|
||||||
|
"""E1: enemy adjacent to reachable food → True."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
enemy = self._enemy((5, 5), [(5, 5), (5, 4), (5, 3)])
|
||||||
|
food_set = {(6, 5)}
|
||||||
|
self.assertTrue(snake._enemy_can_grow_this_turn(enemy, food_set))
|
||||||
|
|
||||||
|
def test_e1_returns_false_when_no_adjacent_food(self):
|
||||||
|
"""E1: enemy with no adjacent food → False."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
enemy = self._enemy((5, 5), [(5, 5), (5, 4), (5, 3)])
|
||||||
|
food_set = {(9, 9)}
|
||||||
|
self.assertFalse(snake._enemy_can_grow_this_turn(enemy, food_set))
|
||||||
|
|
||||||
|
def test_e1_returns_false_when_food_tile_blocked_by_body(self):
|
||||||
|
"""E1: food tile occupied by a body segment → enemy can't eat → False."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
enemy = self._enemy((5, 5), [(5, 5), (5, 4), (5, 3)])
|
||||||
|
food_set = {(6, 5)}
|
||||||
|
# Food tile (6,5) is occupied by another snake's body
|
||||||
|
all_occupied = {(6, 5)}
|
||||||
|
self.assertFalse(
|
||||||
|
snake._enemy_can_grow_this_turn(enemy, food_set, all_occupied),
|
||||||
|
"E1: food tile blocked by body → enemy cannot eat → False",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_e1_hungry_eats_even_when_contested(self):
|
||||||
|
"""E1: hungry enemy (health < 40) eats adjacent food even when contested."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
enemy = self._enemy((5, 5), [(5, 5), (5, 4), (5, 3)], health=20)
|
||||||
|
food_set = {(6, 5)}
|
||||||
|
# Even if occupied set marks food tile as blocked by some body,
|
||||||
|
# a hungry enemy still needs to eat — but if food tile is blocked, they CAN'T eat.
|
||||||
|
# E1 skips blocked tiles regardless of health.
|
||||||
|
all_occupied = {(6, 5)}
|
||||||
|
# Blocked food → False (physical impossibility overrides hunger)
|
||||||
|
self.assertFalse(snake._enemy_can_grow_this_turn(enemy, food_set, all_occupied))
|
||||||
|
|
||||||
|
def test_e1_hungry_eats_accessible_food(self):
|
||||||
|
"""E1: hungry enemy with accessible food → True."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
enemy = self._enemy((5, 5), [(5, 5), (5, 4), (5, 3)], health=15)
|
||||||
|
food_set = {(6, 5)}
|
||||||
|
self.assertTrue(snake._enemy_can_grow_this_turn(enemy, food_set, set()))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tests: E2 — per-turn BFS transposition cache ─────────────────────────────
|
||||||
|
|
||||||
|
class TestBFSCache(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_e2_cache_hit_returns_same_result(self):
|
||||||
|
"""E2: second call with identical args must return cached value."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
snake._bfs_cache = {}
|
||||||
|
snake._bfs_cache_turn = 0
|
||||||
|
blocked = frozenset([(1, 0), (0, 1)])
|
||||||
|
result1 = snake._flood_fill_count((0, 0), set(blocked), 11, 11)
|
||||||
|
result2 = snake._flood_fill_count((0, 0), set(blocked), 11, 11)
|
||||||
|
self.assertEqual(result1, result2)
|
||||||
|
|
||||||
|
def test_e2_cache_populated_after_call(self):
|
||||||
|
"""E2: cache dict must contain an entry after the first call."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
snake._bfs_cache = {}
|
||||||
|
snake._bfs_cache_turn = 0
|
||||||
|
snake._flood_fill_count((5, 5), set(), 11, 11)
|
||||||
|
self.assertGreater(len(snake._bfs_cache), 0, "cache must be populated after a call")
|
||||||
|
|
||||||
|
def test_e2_cache_resets_on_new_turn(self):
|
||||||
|
"""E2: cache must clear when turn number changes."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
snake._bfs_cache = {"stale": 42}
|
||||||
|
snake._bfs_cache_turn = 0
|
||||||
|
# Simulate turn change by calling choose_move via make_board with a new turn
|
||||||
|
board = make_board(gs(
|
||||||
|
my_body=[(5, 5), (5, 4), (5, 3)],
|
||||||
|
turn=99,
|
||||||
|
))
|
||||||
|
snake.choose_move(board)
|
||||||
|
self.assertEqual(snake._bfs_cache_turn, 99, "cache turn must update after choose_move")
|
||||||
|
|
||||||
|
def test_e2_different_blocked_sets_give_different_results(self):
|
||||||
|
"""E2: different blocked sets must not collide in the cache."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
snake._bfs_cache = {}
|
||||||
|
r_open = snake._flood_fill_count((5, 5), set(), 11, 11)
|
||||||
|
r_blocked = snake._flood_fill_count((5, 5), {(5, 6), (6, 5), (4, 5), (5, 4)}, 11, 11)
|
||||||
|
self.assertGreater(r_open, r_blocked, "more blocked cells must reduce reachable space")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tests: E3 — Snail Mode trail scoring ─────────────────────────────────────
|
||||||
|
|
||||||
|
class TestSnailTrailScoring(unittest.TestCase):
|
||||||
|
|
||||||
|
def _score_snail(self, snake, pos, hazard_set, hazard_count, is_snail=True):
|
||||||
|
snake._enemy_dmaps = []
|
||||||
|
snake._enemy_heads = []
|
||||||
|
snake._base_blocked = set()
|
||||||
|
snake._is_snail = is_snail
|
||||||
|
body = [{"x": 5, "y": 5}, {"x": 5, "y": 4}, {"x": 5, "y": 3}]
|
||||||
|
sc, _ = snake._score_move(
|
||||||
|
move="right", pos=pos,
|
||||||
|
my_body=body, my_len=3, my_health=90,
|
||||||
|
other_snakes=[], food_set=set(),
|
||||||
|
hazard_set=hazard_set, hazard_damage=14, hazard_count=hazard_count,
|
||||||
|
previous_hazard_set=hazard_set,
|
||||||
|
is_constrictor=False, enemy_attack_map={},
|
||||||
|
enemy_can_grow={}, total_occupancy=0.05,
|
||||||
|
width=11, height=11, deadline=None,
|
||||||
|
)
|
||||||
|
return sc
|
||||||
|
|
||||||
|
def test_e3_snail_penalises_high_adjacent_hazard_density(self):
|
||||||
|
"""E3: move into area surrounded by hazard scores worse in snail mode."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
pos = {"x": 6, "y": 5}
|
||||||
|
pt = (6, 5)
|
||||||
|
# Neighbours of (6,5): (7,5),(5,5),(6,6),(6,4) — surround with hazard
|
||||||
|
hazard_neighbors = {(7, 5), (6, 6), (6, 4)}
|
||||||
|
score_with_hazard = self._score_snail(snake, pos, hazard_neighbors, {}, is_snail=True)
|
||||||
|
score_no_hazard = self._score_snail(snake, pos, set(), {}, is_snail=True)
|
||||||
|
self.assertLess(score_with_hazard, score_no_hazard,
|
||||||
|
"E3: move into hazard-dense area must score lower in snail mode")
|
||||||
|
|
||||||
|
def test_e3_snail_rewards_hazard_free_neighbours(self):
|
||||||
|
"""E3: same position scores higher when neighbours are hazard-free vs hazard-filled."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
pos = {"x": 6, "y": 5}
|
||||||
|
# No hazard around position — all neighbours are free
|
||||||
|
score_clear = self._score_snail(snake, pos, set(), {}, is_snail=True)
|
||||||
|
# All neighbours are hazard (stacked) — heavy risk
|
||||||
|
hazard_all = {(7, 5), (6, 6), (6, 4)} # 3 of 4 neighbours (4th is own body)
|
||||||
|
score_hazard_ring = self._score_snail(snake, pos, hazard_all, {n: 2 for n in hazard_all}, is_snail=True)
|
||||||
|
self.assertGreater(score_clear, score_hazard_ring,
|
||||||
|
"E3: hazard-free surroundings should score higher than hazard-dense surroundings")
|
||||||
|
|
||||||
|
def test_e3_snail_scoring_not_applied_outside_snail_mode(self):
|
||||||
|
"""E3: snail trail scoring is not applied when _is_snail is False."""
|
||||||
|
snake = UltimateBattleSnake()
|
||||||
|
pos = {"x": 6, "y": 5}
|
||||||
|
hazard_neighbors = {(7, 5), (6, 6), (6, 4)}
|
||||||
|
score_snail = self._score_snail(snake, pos, hazard_neighbors, {}, is_snail=True)
|
||||||
|
score_normal = self._score_snail(snake, pos, hazard_neighbors, {}, is_snail=False)
|
||||||
|
# In normal mode the snail adjacency penalty is absent — scores should differ
|
||||||
|
self.assertLess(score_snail, score_normal,
|
||||||
|
"E3: snail trail penalty must only apply in snail mode")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tests: GameBoard is_ladder source fix ─────────────────────────────────────
|
||||||
|
|
||||||
|
class TestGameBoardIsLadder(unittest.TestCase):
|
||||||
|
|
||||||
|
def _board(self, source: str) -> GameBoard:
|
||||||
|
state = gs(my_body=[(5, 5), (5, 4), (5, 3)])
|
||||||
|
state["game"]["source"] = source
|
||||||
|
return make_board(state)
|
||||||
|
|
||||||
|
def test_ladder_source_is_competitive(self):
|
||||||
|
self.assertTrue(self._board("ladder").is_ladder)
|
||||||
|
|
||||||
|
def test_league_source_is_competitive(self):
|
||||||
|
self.assertTrue(self._board("league").is_ladder)
|
||||||
|
|
||||||
|
def test_arena_source_is_competitive(self):
|
||||||
|
self.assertTrue(self._board("arena").is_ladder)
|
||||||
|
|
||||||
|
def test_custom_source_is_not_competitive(self):
|
||||||
|
self.assertFalse(self._board("custom").is_ladder)
|
||||||
|
|
||||||
|
def test_challenge_source_is_not_competitive(self):
|
||||||
|
self.assertFalse(self._board("challenge").is_ladder)
|
||||||
|
|
||||||
|
def test_tournament_source_is_not_competitive(self):
|
||||||
|
self.assertFalse(self._board("tournament").is_ladder)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user