Skip to content

Commit 07f7adc

Browse files
authored
Merge pull request #171 from Algorithmic-Battle/scoring
Fix scoring function when programs fail
2 parents f701a08 + 9865b30 commit 07f7adc

3 files changed

Lines changed: 30 additions & 21 deletions

File tree

algobattle/battle.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This module contains the :class:`Battle` class, which speciefies how each type of battle is fought and scored,
44
some basic battle types, and related classed.
55
"""
6+
67
from abc import abstractmethod
78
from collections.abc import Iterable
89
from dataclasses import dataclass, field
@@ -201,8 +202,7 @@ async def run(
201202
*,
202203
with_results: Literal[False] = False,
203204
**kwargs: Unpack[RunKwargs],
204-
) -> Fight:
205-
...
205+
) -> Fight: ...
206206

207207
@overload
208208
async def run(
@@ -211,8 +211,7 @@ async def run(
211211
*,
212212
with_results: Literal[True],
213213
**kwargs: Unpack[RunKwargs],
214-
) -> tuple[Fight, GeneratorResult, SolverResult | None]:
215-
...
214+
) -> tuple[Fight, GeneratorResult, SolverResult | None]: ...
216215

217216
async def run(
218217
self,
@@ -250,12 +249,7 @@ async def run(
250249
The resulting info about the executed fight, and the results if the flag has been set.
251250
"""
252251
gen_result, sol_result = await self.run_raw(max_size=max_size, **kwargs)
253-
if gen_result.instance is None or gen_result.solution is None:
254-
score = 1
255-
elif sol_result is None or sol_result.solution is None:
256-
score = 0
257-
else:
258-
score = self.calculate_score(gen_result, sol_result)
252+
score = self.calculate_score(gen_result, sol_result)
259253
fight = Fight.from_results(
260254
score=score,
261255
max_size=max_size,
@@ -321,7 +315,7 @@ async def run_raw(
321315
)
322316
return gen_result, sol_result
323317

324-
def calculate_score(self, gen_result: GeneratorResult, sol_result: SolverResult) -> float:
318+
def calculate_score(self, gen_result: GeneratorResult, sol_result: SolverResult | None) -> float:
325319
"""Calculates the score achieved by the solver in this fight.
326320
327321
Both results need to contain all instance and/or solution data required.
@@ -333,9 +327,25 @@ def calculate_score(self, gen_result: GeneratorResult, sol_result: SolverResult)
333327
Returns:
334328
A number in [0, 1] with higher numbers meaning the solver performed better.
335329
"""
336-
assert gen_result.instance is not None
337-
assert sol_result.solution is not None
330+
# We first need to check whether the generator somehow failed. This can happen in three cases:
331+
# it doesn't produce an instance, it creates an invalid instance or it doesn't create a needed solution
332+
# note that gen_result.instance and gen_result.error will both contain data if the generator outputted an
333+
# invalid instance and/or solution
334+
if (
335+
gen_result.instance is None
336+
or gen_result.error is not None
337+
or (self.problem.with_solution and gen_result.solution is None)
338+
):
339+
return 1
340+
# sol_result is None only if the generator failed, which needs to be caught by the above check
341+
assert sol_result is not None
342+
# The solver failed if it didn't output a solution or the solution contains some error
343+
if sol_result.solution is None or sol_result.error is not None:
344+
return 0
345+
338346
if self.problem.with_solution:
347+
# we need this assert since type checkers can't see the dependency between self.problem.with_solution
348+
# and gen_result.solution that is created by the last check in the first "if"
339349
assert gen_result.solution is not None
340350
score = self.problem.score(
341351
gen_result.instance, solver_solution=sol_result.solution, generator_solution=gen_result.solution
@@ -449,8 +459,7 @@ class FallbackConfig(Config):
449459

450460
if TYPE_CHECKING:
451461
# to hint that we're gonna fill this with arbitrary data belonging to some supposed battle type
452-
def __getattr__(self, attr: str, /) -> Any:
453-
...
462+
def __getattr__(self, attr: str, /) -> Any: ...
454463

455464
class UiData(BaseModel):
456465
"""Object containing custom diplay data.
@@ -705,13 +714,13 @@ class Config(Battle.Config):
705714
"""Number of fights that will be fought."""
706715
weighting: Annotated[float, Ge(0)] = 1.1
707716
"""How much each successive fight should be weighted more than the previous."""
708-
scores: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
717+
scores: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
709718
"""Who to show each fight's scores to."""
710-
instances: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
719+
instances: set[Role] = {Role.generator, Role.solver} # noqa: RUF012
711720
"""Who to show the instances to."""
712-
generator_solutions: set[Role] = {Role.generator} # noqa: RUF012
721+
generator_solutions: set[Role] = {Role.generator} # noqa: RUF012
713722
"""Who to show the generator's solutions to, if the problem requires them."""
714-
solver_solutions: set[Role] = {Role.solver} # noqa: RUF012
723+
solver_solutions: set[Role] = {Role.solver} # noqa: RUF012
715724
"""Who to show the solver's solutions to."""
716725

717726
class UiData(Battle.UiData):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "pdm.backend"
44

55
[project]
66
name = "algobattle-base"
7-
version = "4.3.3"
7+
version = "4.3.4"
88
description = "The Algobattle lab course package."
99
readme = "README.md"
1010
requires-python = ">=3.11"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)