44some basic battle types, and related classed.
55"""
66from dataclasses import dataclass
7+ from enum import StrEnum
78from functools import wraps
89from importlib .metadata import entry_points
910from abc import abstractmethod
3031 ConfigDict ,
3132 Field ,
3233 GetCoreSchemaHandler ,
34+ SerializeAsAny ,
3335 ValidationError ,
3436 ValidationInfo ,
3537 ValidatorFunctionWrapHandler ,
3941
4042from algobattle .program import (
4143 Generator ,
42- ProgramRunInfo ,
44+ GeneratorResult ,
45+ ProgramResult ,
4346 ProgramUi ,
47+ RunConfigOverride ,
4448 Solver ,
49+ SolverResult ,
50+ )
51+ from algobattle .problem import InstanceModel , Problem , SolutionModel
52+ from algobattle .util import (
53+ Encodable ,
54+ EncodableModel ,
55+ ExceptionInfo ,
56+ BaseModel ,
4557)
46- from algobattle .problem import Problem
47- from algobattle .util import Encodable , ExceptionInfo , BaseModel
4858
4959
5060_BattleConfig : TypeAlias = Any
6171Type = type
6272
6373
74+ class ProgramLogConfigTime (StrEnum ):
75+ """When to log a programs i/o."""
76+
77+ never = "never"
78+ error = "error"
79+ always = "always"
80+
81+
82+ class ProgramLogConfigLocation (StrEnum ):
83+ """Where to log a programs i/o."""
84+
85+ disabled = "disabled"
86+ inline = "inline"
87+
88+
89+ class ProgramLogConfigView (Protocol ): # noqa: D101
90+ when : ProgramLogConfigTime = ProgramLogConfigTime .error
91+ output : ProgramLogConfigLocation = ProgramLogConfigLocation .inline
92+
93+
94+ class ProgramRunInfo (BaseModel ):
95+ """Data about a program's execution."""
96+
97+ runtime : float = 0
98+ overriden : RunConfigOverride = Field (default_factory = dict )
99+ error : ExceptionInfo | None = None
100+ battle_data : SerializeAsAny [EncodableModel ] | None = None
101+ instance : SerializeAsAny [InstanceModel ] | None = None
102+ solution : SerializeAsAny [SolutionModel [InstanceModel ]] | None = None
103+
104+ @classmethod
105+ def from_result (cls , result : ProgramResult , * , inline_output : bool ) -> Self :
106+ """Converts the program run info into a jsonable model."""
107+ info = cls (
108+ runtime = result .runtime ,
109+ overriden = result .overriden ,
110+ error = result .error ,
111+ )
112+ if inline_output :
113+ if isinstance (result .battle_data , EncodableModel ):
114+ info .battle_data = result .battle_data
115+ if isinstance (result .solution , SolutionModel ):
116+ info .solution = result .solution
117+ if isinstance (result , GeneratorResult ) and isinstance (result .instance , InstanceModel ):
118+ info .instance = result .instance
119+ return info
120+
121+
64122class Fight (BaseModel ):
65123 """The result of one fight between the participating teams.
66124
@@ -79,6 +137,28 @@ class Fight(BaseModel):
79137 solver : ProgramRunInfo | None
80138 """Data about the solver's execution."""
81139
140+ @classmethod
141+ def from_results (
142+ cls ,
143+ max_size : int ,
144+ score : float ,
145+ generator : GeneratorResult ,
146+ solver : SolverResult | None ,
147+ * ,
148+ config : ProgramLogConfigView ,
149+ ) -> Self :
150+ """Turns the involved result objects into a jsonable model."""
151+ inline_output = config .when == "always" or (
152+ config .when == "error"
153+ and (generator .error is not None or (solver is not None and solver .error is not None ))
154+ )
155+ return cls (
156+ max_size = max_size ,
157+ score = score ,
158+ generator = ProgramRunInfo .from_result (generator , inline_output = inline_output ),
159+ solver = ProgramRunInfo .from_result (solver , inline_output = inline_output ) if solver is not None else None ,
160+ )
161+
82162
83163class FightUi (ProgramUi , Protocol ):
84164 """Provides an interface for :class:`Fight` to update the ui."""
@@ -113,6 +193,7 @@ class FightHandler:
113193 battle : "Battle"
114194 ui : FightUi
115195 set_cpus : str | None
196+ log_config : ProgramLogConfigView
116197
117198 @_save_result
118199 async def run (
@@ -175,8 +256,14 @@ async def run(
175256 set_cpus = self .set_cpus ,
176257 ui = ui ,
177258 )
178- if gen_result .info .error is not None :
179- return Fight (score = 1 , max_size = max_size , generator = gen_result .info , solver = None )
259+ if gen_result .error is not None :
260+ return Fight .from_results (
261+ score = 1 ,
262+ max_size = max_size ,
263+ generator = gen_result ,
264+ solver = None ,
265+ config = self .log_config ,
266+ )
180267 assert gen_result .instance is not None
181268
182269 sol_result = await self .solver .run (
@@ -190,8 +277,10 @@ async def run(
190277 set_cpus = self .set_cpus ,
191278 ui = ui ,
192279 )
193- if sol_result .info .error is not None :
194- return Fight (score = 0 , max_size = max_size , generator = gen_result .info , solver = sol_result .info )
280+ if sol_result .error is not None :
281+ return Fight .from_results (
282+ score = 0 , max_size = max_size , generator = gen_result , solver = sol_result , config = self .log_config
283+ )
195284 assert sol_result .solution is not None
196285
197286 if self .problem .with_solution :
@@ -202,7 +291,13 @@ async def run(
202291 else :
203292 score = self .problem .score (gen_result .instance , solution = sol_result .solution )
204293 score = max (0 , min (1 , float (score )))
205- return Fight (score = score , max_size = max_size , generator = gen_result .info , solver = sol_result .info )
294+ return Fight .from_results (
295+ score = score ,
296+ max_size = max_size ,
297+ generator = gen_result ,
298+ solver = sol_result ,
299+ config = self .log_config ,
300+ )
206301
207302
208303# We need this to be here to prevent an import cycle between match.py and battle.py
0 commit comments