algobattle-base 4.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. algobattle_base-4.0.0rc1/LICENSE +21 -0
  2. algobattle_base-4.0.0rc1/PKG-INFO +69 -0
  3. algobattle_base-4.0.0rc1/README.md +48 -0
  4. algobattle_base-4.0.0rc1/algobattle/__init__.py +4 -0
  5. algobattle_base-4.0.0rc1/algobattle/battle.py +394 -0
  6. algobattle_base-4.0.0rc1/algobattle/cli.py +363 -0
  7. algobattle_base-4.0.0rc1/algobattle/docker_util.py +870 -0
  8. algobattle_base-4.0.0rc1/algobattle/match.py +358 -0
  9. algobattle_base-4.0.0rc1/algobattle/problem.py +312 -0
  10. algobattle_base-4.0.0rc1/algobattle/team.py +190 -0
  11. algobattle_base-4.0.0rc1/algobattle/util.py +276 -0
  12. algobattle_base-4.0.0rc1/pyproject.toml +47 -0
  13. algobattle_base-4.0.0rc1/tests/README.md +7 -0
  14. algobattle_base-4.0.0rc1/tests/__init__.py +0 -0
  15. algobattle_base-4.0.0rc1/tests/configs/empty.toml +0 -0
  16. algobattle_base-4.0.0rc1/tests/configs/teams.toml +7 -0
  17. algobattle_base-4.0.0rc1/tests/configs/teams_incorrect.toml +6 -0
  18. algobattle_base-4.0.0rc1/tests/configs/test.toml +9 -0
  19. algobattle_base-4.0.0rc1/tests/test_docker.py +168 -0
  20. algobattle_base-4.0.0rc1/tests/test_match.py +250 -0
  21. algobattle_base-4.0.0rc1/tests/test_util.py +40 -0
  22. algobattle_base-4.0.0rc1/tests/testsproblem/README.md +22 -0
  23. algobattle_base-4.0.0rc1/tests/testsproblem/__init__.py +5 -0
  24. algobattle_base-4.0.0rc1/tests/testsproblem/build_error/Dockerfile +3 -0
  25. algobattle_base-4.0.0rc1/tests/testsproblem/build_timeout/Dockerfile +3 -0
  26. algobattle_base-4.0.0rc1/tests/testsproblem/generator/Dockerfile +5 -0
  27. algobattle_base-4.0.0rc1/tests/testsproblem/generator/instance.json +3 -0
  28. algobattle_base-4.0.0rc1/tests/testsproblem/generator_execution_error/Dockerfile +4 -0
  29. algobattle_base-4.0.0rc1/tests/testsproblem/generator_semantics_error/Dockerfile +5 -0
  30. algobattle_base-4.0.0rc1/tests/testsproblem/generator_semantics_error/instance.json +3 -0
  31. algobattle_base-4.0.0rc1/tests/testsproblem/generator_syntax_error/Dockerfile +5 -0
  32. algobattle_base-4.0.0rc1/tests/testsproblem/generator_syntax_error/instance.json +0 -0
  33. algobattle_base-4.0.0rc1/tests/testsproblem/generator_timeout/Dockerfile +5 -0
  34. algobattle_base-4.0.0rc1/tests/testsproblem/generator_timeout/instance.json +3 -0
  35. algobattle_base-4.0.0rc1/tests/testsproblem/problem.py +35 -0
  36. algobattle_base-4.0.0rc1/tests/testsproblem/solver/Dockerfile +5 -0
  37. algobattle_base-4.0.0rc1/tests/testsproblem/solver/solution.json +4 -0
  38. algobattle_base-4.0.0rc1/tests/testsproblem/solver_execution_error/Dockerfile +4 -0
  39. algobattle_base-4.0.0rc1/tests/testsproblem/solver_semantics_error/Dockerfile +5 -0
  40. algobattle_base-4.0.0rc1/tests/testsproblem/solver_semantics_error/solution.json +4 -0
  41. algobattle_base-4.0.0rc1/tests/testsproblem/solver_syntax_error/Dockerfile +5 -0
  42. algobattle_base-4.0.0rc1/tests/testsproblem/solver_syntax_error/solution.json +0 -0
  43. algobattle_base-4.0.0rc1/tests/testsproblem/solver_timeout/Dockerfile +5 -0
  44. algobattle_base-4.0.0rc1/tests/testsproblem/solver_timeout/solution.json +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Henri Lotze
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.1
2
+ Name: algobattle-base
3
+ Version: 4.0.0rc1
4
+ Summary: The Algobattle lab course package.
5
+ Author: Imogen Hergeth, Jan Dreier, Henri Lotze
6
+ License: MIT
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Environment :: Console
9
+ Classifier: Natural Language :: English
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Project-URL: Github, https://github.com/Benezivas/algobattle
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: docker>=6.1.2
16
+ Requires-Dist: prettytable>=3.7.0
17
+ Requires-Dist: pydantic~=1.10
18
+ Requires-Dist: anyio>=3.6.2
19
+ Requires-Dist: windows-curses>=2.3.1; os_name == "nt"
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Algorithmic Battle
23
+
24
+ The lab course "Algorithmic Battle" is offered by the
25
+ [Computer Science Theory group of RWTH Aachen University](https://tcs.rwth-aachen.de/)
26
+ since 2019. This repository contains the necessary code and documentation to
27
+ set up the lab course yourself.
28
+
29
+ The idea of the lab is to pose several, usually NP-complete problems during the
30
+ semester.
31
+ Groups of students then write code that generates hard-to-solve instances for
32
+ these problems and solvers that solve these problems quickly. In the default
33
+ setting, the groups then battle against each other, generating instances of
34
+ increasing size that the other group has to solve within a time limit.
35
+ Points are distributed relative to the biggest instance size for which a group
36
+ was still able to solve an instance.
37
+
38
+ # Installation
39
+ This project is being developed and tested on both Windows and Linux, MacOS support
40
+ is being worked on but still is tentative.
41
+
42
+ `python3` in version at least `3.10` and `docker` are required.
43
+
44
+ We recommend installing the package as a user using `pip`
45
+ ```
46
+ pip install . --user
47
+ ```
48
+
49
+ Adjust the parameters set in the `algobattle/config.ini` file to set
50
+ which hardware resources you want to assign. You can pass alternative
51
+ configuration files to the script using the `--config_file` option.
52
+
53
+
54
+ # Usage
55
+ This repository does not include any practical problems. For a selection of problems
56
+ that have been posed to students in practice, have a look at the
57
+ [algobattle-problems](https://github.com/Benezivas/algobattle-problems) repository.
58
+
59
+ To start a basic run on a problem, using the `solver` and `generator` that
60
+ are part of the problem directory, use
61
+ ```
62
+ algobattle path/to/concrecte/problem/folder
63
+ ```
64
+
65
+ The `algobattle` script offers several options, e.g. to give custom paths for
66
+ solvers and generators. Run `algobattle --help` for all options.
67
+
68
+
69
+ Check the [wiki](https://github.com/Benezivas/algobattle/wiki) for further documentation.
@@ -0,0 +1,48 @@
1
+ # Algorithmic Battle
2
+
3
+ The lab course "Algorithmic Battle" is offered by the
4
+ [Computer Science Theory group of RWTH Aachen University](https://tcs.rwth-aachen.de/)
5
+ since 2019. This repository contains the necessary code and documentation to
6
+ set up the lab course yourself.
7
+
8
+ The idea of the lab is to pose several, usually NP-complete problems during the
9
+ semester.
10
+ Groups of students then write code that generates hard-to-solve instances for
11
+ these problems and solvers that solve these problems quickly. In the default
12
+ setting, the groups then battle against each other, generating instances of
13
+ increasing size that the other group has to solve within a time limit.
14
+ Points are distributed relative to the biggest instance size for which a group
15
+ was still able to solve an instance.
16
+
17
+ # Installation
18
+ This project is being developed and tested on both Windows and Linux, MacOS support
19
+ is being worked on but still is tentative.
20
+
21
+ `python3` in version at least `3.10` and `docker` are required.
22
+
23
+ We recommend installing the package as a user using `pip`
24
+ ```
25
+ pip install . --user
26
+ ```
27
+
28
+ Adjust the parameters set in the `algobattle/config.ini` file to set
29
+ which hardware resources you want to assign. You can pass alternative
30
+ configuration files to the script using the `--config_file` option.
31
+
32
+
33
+ # Usage
34
+ This repository does not include any practical problems. For a selection of problems
35
+ that have been posed to students in practice, have a look at the
36
+ [algobattle-problems](https://github.com/Benezivas/algobattle-problems) repository.
37
+
38
+ To start a basic run on a problem, using the `solver` and `generator` that
39
+ are part of the problem directory, use
40
+ ```
41
+ algobattle path/to/concrecte/problem/folder
42
+ ```
43
+
44
+ The `algobattle` script offers several options, e.g. to give custom paths for
45
+ solvers and generators. Run `algobattle --help` for all options.
46
+
47
+
48
+ Check the [wiki](https://github.com/Benezivas/algobattle/wiki) for further documentation.
@@ -0,0 +1,4 @@
1
+ """The base algobattle package.
2
+
3
+ Contains the code to run matches, default battle types, and Problem parent classes.
4
+ """
@@ -0,0 +1,394 @@
1
+ """A Battle is the portion of a match where a specific team generates instances and another one solves them.
2
+
3
+ This module contains the :cls:`Battle` class, which speciefies how each type of battle is fought and scored,
4
+ some basic battle types, and related classed.
5
+ """
6
+ from dataclasses import dataclass
7
+ from importlib.metadata import entry_points
8
+ from abc import abstractmethod
9
+ from typing import (
10
+ Any,
11
+ ClassVar,
12
+ Protocol,
13
+ TypeAlias,
14
+ TypeVar,
15
+ )
16
+
17
+ from pydantic import Field
18
+
19
+ from algobattle.docker_util import (
20
+ Generator,
21
+ ProgramRunInfo,
22
+ ProgramUiProxy,
23
+ Solver,
24
+ )
25
+ from algobattle.util import Encodable, Role, inherit_docs, BaseModel
26
+
27
+
28
+ _BattleConfig: TypeAlias = Any
29
+ """Type alias used to generate correct typings when subclassing :cls:`Battle`.
30
+
31
+ Each battle type's :meth:`run` method is guaranteed to be passed an instance of its own :cls:`BattleConfig` object.
32
+ But due to limitations in the python type system we are currently not able to express this properly.
33
+ When creating your own battle type it is recommended to not use this alias and instead use the :cls:`BattleConfig` of
34
+ the new battle type directly.
35
+ """
36
+ T = TypeVar("T")
37
+
38
+
39
+ class Fight(BaseModel):
40
+ """The result of one fight between the participating teams.
41
+
42
+ For a more detailed description of what each fight looks like, see :meth:`FightHandler.run`.
43
+ """
44
+
45
+ score: float
46
+ """The solving Team's score.
47
+
48
+ Always a number in [0, 1]. 0 indicates a total failure of the solver, 1 that it succeeded perfectly.
49
+ """
50
+ max_size: int
51
+ """The maximum size of an instance the generator was allowed to create."""
52
+ generator: ProgramRunInfo
53
+ """Data about the generator's execution."""
54
+ solver: ProgramRunInfo | None
55
+ """Data about the solver's execution."""
56
+
57
+
58
+ class FightUiProxy(Protocol):
59
+ """Provides an interface for :cls:`Fight` to update the ui."""
60
+
61
+ generator: ProgramUiProxy
62
+ solver: ProgramUiProxy
63
+
64
+ @abstractmethod
65
+ def start(self, max_size: int) -> None:
66
+ """Informs the ui that a new fight has been started."""
67
+
68
+ @abstractmethod
69
+ def update(self, role: Role, data: ProgramRunInfo) -> None:
70
+ """Updates the ui's current fight section with new data about a program."""
71
+
72
+ @abstractmethod
73
+ def end(self) -> None:
74
+ """Informs the ui that the fight has finished running and has been added to the battle's `.fight_results`."""
75
+
76
+
77
+ @dataclass
78
+ class FightHandler:
79
+ """Helper class to run fights of a given battle."""
80
+
81
+ _generator: Generator
82
+ _solver: Solver
83
+ _battle: "Battle"
84
+ _ui: FightUiProxy
85
+ _set_cpus: str | None = None
86
+
87
+ def _saved(self, fight: Fight) -> Fight:
88
+ self._battle.fight_results.append(fight)
89
+ self._ui.end()
90
+ return fight
91
+
92
+ async def run(
93
+ self,
94
+ max_size: int,
95
+ *,
96
+ timeout_generator: float | None = ...,
97
+ space_generator: int | None = ...,
98
+ cpus_generator: int = ...,
99
+ timeout_solver: float | None = ...,
100
+ space_solver: int | None = ...,
101
+ cpus_solver: int = ...,
102
+ generator_battle_input: Encodable | None = None,
103
+ solver_battle_input: Encodable | None = None,
104
+ generator_battle_output: type[Encodable] | None = None,
105
+ solver_battle_output: type[Encodable] | None = None,
106
+ ) -> Fight:
107
+ """Execute a single fight of a battle.
108
+
109
+ First the generator will be run and its output parsed. Then the solver will be given the created instance
110
+ and run. Its output gets parsed into a solution, which will then be scored.
111
+ The timeout, space, and cpu arguments each override the corresponding match config options if set. Leaving them
112
+ unset results in the config options being used.
113
+
114
+ Args:
115
+ max_size: The maximum instance size the generator is allowed to create.
116
+ timeout_generator: Timeout in seconds for the generator to finish running. `None` means it is given an
117
+ unlimited amount of time.
118
+ space_generator: Memory space in MB the generator has access to. `None` means it is given an unlimited
119
+ amount of space.
120
+ cpus_generator: Number of physical cpu cores the generator can use.
121
+ timeout_solver: Timeout in seconds for the solver to finish running. `None` means it is given an unlimited
122
+ amount of time.
123
+ space_solver: Memory space in MB the solver has access to. `None` means it is given
124
+ an unlimited amount of space.
125
+ cpus_solver: Number of physical cpu cores the solver can use.
126
+ generator_battle_input: Additional data the generator will be provided with.
127
+ solver_battle_input: Additional data the solver will be provided with.
128
+ generator_battle_output: Class used to parse additional data the generator outputs into a python object.
129
+ solver_battle_output: Class used to parse additional data the solver outputs into a python object.
130
+
131
+ Returns:
132
+ The resulting info about the executed fight.
133
+ """
134
+ min_size = self._generator.problem_class.min_size
135
+ if max_size < min_size:
136
+ raise ValueError(
137
+ f"Cannot run battle at size {max_size} since it is smaller than the smallest "
138
+ "size the problem allows ({min_size})."
139
+ )
140
+ ui = self._ui
141
+ ui.start(max_size)
142
+ gen_result = await self._generator.run(
143
+ max_size=max_size,
144
+ timeout=timeout_generator,
145
+ space=space_generator,
146
+ cpus=cpus_generator,
147
+ battle_input=generator_battle_input,
148
+ battle_output=generator_battle_output,
149
+ set_cpus=self._set_cpus,
150
+ ui=ui.generator,
151
+ )
152
+ ui.update(Role.generator, gen_result.info)
153
+ if gen_result.instance is None:
154
+ return self._saved(Fight(score=1, max_size=max_size, generator=gen_result.info, solver=None))
155
+
156
+ sol_result = await self._solver.run(
157
+ gen_result.instance,
158
+ max_size=max_size,
159
+ timeout=timeout_solver,
160
+ space=space_solver,
161
+ cpus=cpus_solver,
162
+ battle_input=solver_battle_input,
163
+ battle_output=solver_battle_output,
164
+ set_cpus=self._set_cpus,
165
+ ui=ui.solver,
166
+ )
167
+ ui.update(Role.solver, sol_result.info)
168
+ if sol_result.solution is None:
169
+ return self._saved(Fight(score=0, max_size=max_size, generator=gen_result.info, solver=sol_result.info))
170
+
171
+ score = gen_result.instance.score(solver_solution=sol_result.solution, generator_solution=gen_result.solution)
172
+ score = max(0, min(1, float(score)))
173
+ return self._saved(Fight(score=score, max_size=max_size, generator=gen_result.info, solver=sol_result.info))
174
+
175
+
176
+ # We need this to be here to prevent an import cycle between match.py and battle.py
177
+ class BattleUiProxy(Protocol):
178
+ """Provides an interface for :cls:`Battle`s to update the Ui."""
179
+
180
+ fight_ui: FightUiProxy
181
+
182
+ @abstractmethod
183
+ def update_data(self, data: "Battle.UiData") -> None:
184
+ """Passes new custom display data to the Ui.
185
+
186
+ See :cls:`Battle.UiData` for further details.
187
+ """
188
+
189
+
190
+ class Battle(BaseModel):
191
+ """Base for classes that execute a specific kind of battle.
192
+
193
+ Each battle type determines what parameters each fight will be fought with, how many fights are fought, and how
194
+ they will ultimately be scored.
195
+ """
196
+
197
+ fight_results: list[Fight] = Field(default_factory=list)
198
+ """The list of fights that have been fought in this battle."""
199
+ run_exception: str | None = None
200
+ """The description of an otherwise unhandeled exception that occured during the execution of :meth:`.run`."""
201
+
202
+ _battle_types: ClassVar[dict[str, type["Battle"]]] = {}
203
+ """Dictionary mapping the names of all registered battle types to their python classes."""
204
+
205
+ class BattleConfig(BaseModel):
206
+ """Config object for each specific battle type.
207
+
208
+ A custom battle type can override this class to specify config options it uses. They will be parsed from a
209
+ dictionary located at `battle.NAME` in the main config file, where NAME is the specific batle type's name.
210
+ The created object will then be passed to the :meth:`.run` method with its fields set accordingly.
211
+ """
212
+
213
+ class UiData(BaseModel):
214
+ """Object containing custom diplay data.
215
+
216
+ The display data object will be displayed as key-value pairs generated from the :meth:`.field` method.
217
+ You can use the normally available pydantic config options to customize what these will look like.
218
+ """
219
+
220
+ @staticmethod
221
+ def all() -> dict[str, type["Battle"]]:
222
+ """Returns a dictionary mapping the names of all registered battle types to their python classes.
223
+
224
+ It includes all subclasses of :cls:`Battle` that have been initialized so far, including ones exposed to the
225
+ algobattle module via the `algobattle.battle` entrypoint hook.
226
+ """
227
+ for entrypoint in entry_points(group="algobattle.battle"):
228
+ if entrypoint.name not in Battle._battle_types:
229
+ battle: type[Battle] = entrypoint.load()
230
+ Battle._battle_types[battle.name()] = battle
231
+ return Battle._battle_types
232
+
233
+ def __init_subclass__(cls) -> None:
234
+ if cls.name() not in Battle._battle_types:
235
+ Battle._battle_types[cls.name()] = cls
236
+ return super().__init_subclass__()
237
+
238
+ @abstractmethod
239
+ def score(self) -> float:
240
+ """Calculates the score the solver has achieved during this battle.
241
+
242
+ Should always be a nonnegative float, with higher values indicating a better performance of the solver.
243
+ """
244
+ raise NotImplementedError
245
+
246
+ @staticmethod
247
+ def format_score(score: float) -> str:
248
+ """Formats a given score nicely.
249
+
250
+ Purely auxialiary method that can be used to customize how a score will be rendered.
251
+ """
252
+ return f"{score:.2f}"
253
+
254
+ @classmethod
255
+ def name(cls) -> str:
256
+ """Name of this battle type.
257
+
258
+ Defaults to the battle class's name. Can be used to customize this behaviour if e.g. a battle type should have a
259
+ name that is not a valid python identifier.
260
+ """
261
+ return cls.__name__
262
+
263
+ @abstractmethod
264
+ async def run_battle(self, fight: FightHandler, config: _BattleConfig, min_size: int, ui: BattleUiProxy) -> None:
265
+ """Executes one battle.
266
+
267
+ Args:
268
+ fight: The :cls:`FightHandler` used to run each fight of this battle. It already contains information about
269
+ the participating teams, default config settings, etc. Each fight can be executed using its :meth:`.run`
270
+ method.
271
+ config: An instance of this battle type's :cls:`BattleConfig` class, parsed from the corresponding section
272
+ of the config file.
273
+ min_size: The minimum size valid for this problem.
274
+ ui: An interface to interact with the ui.
275
+ """
276
+ raise NotImplementedError
277
+
278
+
279
+ class Iterated(Battle):
280
+ """Class that executes an iterated battle."""
281
+
282
+ results: list[int] = Field(default_factory=list)
283
+
284
+ @inherit_docs
285
+ class BattleConfig(Battle.BattleConfig):
286
+ rounds: int = 5
287
+ """Number of times the instance size will be increased until the solver fails to produce correct solutions."""
288
+ maximum_size: int = 50_000
289
+ """Maximum instance size that will be tried."""
290
+ exponent: int = 2
291
+ """Determines how quickly the instance size grows."""
292
+ minimum_score: float = 1
293
+ """Minimum score that a solver needs to achieve in order to pass."""
294
+
295
+ @inherit_docs
296
+ class UiData(Battle.UiData):
297
+ reached: list[int]
298
+ cap: int
299
+
300
+ async def run_battle(self, fight: FightHandler, config: BattleConfig, min_size: int, ui: BattleUiProxy) -> None:
301
+ """Execute an iterated battle.
302
+
303
+ Incrementally tries to search for the highest n for which the solver is still able to solve instances.
304
+ The base increment value is multiplied with the number of iterations since the last unsolvable instance to the
305
+ given exponent. Only once the solver fails directly after the multiplier is reset, it counts as failed. Since
306
+ this would heavily favour probabilistic algorithms (That may have only failed by chance and are able to solve a
307
+ certain instance size on a second try), we cap the maximum solution size by the last value that an algorithm
308
+ has failed on. If the solver never stops, the battle will run until the instance size reaches `iteration_cap`.
309
+
310
+ This process is repeated `rounds` many times, with each round being completely independent of each other.
311
+ """
312
+ for _ in range(config.rounds):
313
+ base_increment = 0
314
+ alive = True
315
+ reached = 0
316
+ cap = config.maximum_size
317
+ current = min_size
318
+ while alive:
319
+ ui.update_data(self.UiData(reached=self.results + [reached], cap=cap))
320
+ result = await fight.run(current)
321
+ score = result.score
322
+ if score < config.minimum_score:
323
+ alive = False
324
+
325
+ if not alive and base_increment > 1:
326
+ # The step size increase was too aggressive, take it back and reset the base_increment
327
+ cap = current
328
+ current -= base_increment**config.exponent
329
+ base_increment = 0
330
+ alive = True
331
+ elif current > reached and alive:
332
+ # We solved an instance of bigger size than before
333
+ reached = current
334
+
335
+ if current + 1 > cap:
336
+ alive = False
337
+ else:
338
+ base_increment += 1
339
+ current += base_increment**config.exponent
340
+
341
+ if current >= cap:
342
+ # We have failed at this value of n already, reset the step size!
343
+ current -= base_increment**config.exponent - 1
344
+ base_increment = 1
345
+ self.results.append(reached)
346
+
347
+ @inherit_docs
348
+ def score(self) -> float:
349
+ """Averages the highest instance size reached in each round."""
350
+ return 0 if len(self.results) == 0 else sum(self.results) / len(self.results)
351
+
352
+ @inherit_docs
353
+ @staticmethod
354
+ def format_score(score: float) -> str:
355
+ return str(int(score))
356
+
357
+
358
+ class Averaged(Battle):
359
+ """Class that executes an averaged battle."""
360
+
361
+ @inherit_docs
362
+ class BattleConfig(Battle.BattleConfig):
363
+ instance_size: int = 10
364
+ """Instance size that will be fought at."""
365
+ num_fights: int = 10
366
+ """Number of iterations in each round."""
367
+
368
+ @inherit_docs
369
+ class UiData(Battle.UiData):
370
+ round: int
371
+
372
+ async def run_battle(self, fight: FightHandler, config: BattleConfig, min_size: int, ui: BattleUiProxy) -> None:
373
+ """Execute an averaged battle.
374
+
375
+ This simple battle type just executes `iterations` many fights after each other at size `instance_size`.
376
+ """
377
+ if config.instance_size < min_size:
378
+ raise ValueError(f"size {config.instance_size} is smaller than the smallest valid size, {min_size}.")
379
+ for i in range(config.num_fights):
380
+ ui.update_data(self.UiData(round=i + 1))
381
+ await fight.run(config.instance_size)
382
+
383
+ @inherit_docs
384
+ def score(self) -> float:
385
+ """Averages the score of each fight."""
386
+ if len(self.fight_results) == 0:
387
+ return 0
388
+ else:
389
+ return sum(f.score for f in self.fight_results) / len(self.fight_results)
390
+
391
+ @inherit_docs
392
+ @staticmethod
393
+ def format_score(score: float) -> str:
394
+ return format(score, ".0%")