-
Notifications
You must be signed in to change notification settings - Fork 0
Description
GLIR: Gamms Game Record
The issue details a method/proposal to structure a robust method for recording games. The idea is to create a Game Level Intermediate Representation (GLIR) which can represent all actions that references the Gamms context. The gist is to save a sequence calls in a file according to the sequence it was called in the original game. During replay, the calls are read and then actually called to emulate the game. The base supported functionality should be as follows:
- Start/Pause/Play/Stop recording the calls where start/stop records the beginning and end of the record
- Replay should load one call at a time and allow to access the entire game state between every call
- Possible to record a game that is being replayed from a file
- Add extra calls or do extra calls between individual replays
- Allow to note down game specific state to a certain extent
Given the above properties, the following behaviors are expected:
- Replaying and recoding the same game at the same time with same versions should act as a copy (Correctness Test).
- Replaying an older version game will record it in a newer version if the recording protocol is of a higher version.
- While recording, if a game is replayed with modified rule/tracking calls, the new recording should save those calls. However, it may cause that multiple calls happen at the same time which also acts as a test for knowing modifications.
- Time mismatch will not happen recording old games to newer versions.
Recorder
A new object called the recorder needs to be added in the Context. The recorder object will manage any and all implementation details related to saving and loading games. It will also provide the memory to track game specific state data. The following is the interface for the recorder.
class IRecorder:
def record(self) -> bool:
"""
Boolean to inform whether game is being recorded or not and ctx is alive
"""
pass
def start(self, path: str) -> None:
"""
Start recording to the path. Raise error if file already exists
"""
pass
def stop(self, path: str) -> None:
"""
Stop recording to the path and close the file handler.
"""
pass
def pause(self) -> None:
"""
Pause the recording process. `self.record()` should return false if paused. If not started or stopped, give warning.
"""
pass
def play(self, path: str) -> None:
"""
Resume recording if paused. If not started or stopped, give warning.
"""
pass
def replay(self, path: str) -> Iterator:
"""
Checks validity of the file and output an iterator.
"""
pass
def time(self) -> int64:
"""
Return record time if replaying. Else return the local time `(time.time())`
"""
pass
def write(self, opCode, data) -> None:
"""
Write to record buffer if recording. If not recording raise error as it should not happen.
"""
pass
def memory_get(self, keys: List[Union[str, int]]):
"""
Return the value in the memory for the nested keys
"""
pass
def memory_set(self, keys: List[Union[str, int]], value):
"""
Set the value for the nested key. If second last key does not already exist then raise KeyError
"""
passThe memory in the recorder is for game specific tracking that is defined by the user. All tracked objects should be json serializable. Memory set calls need to be written in the record and as such it requires a hook.
Implementing Interface hooks
Add a condition check on recording inside every user accessible interface calls and use the IRecorder.write to record that particular call. The write call should pickle the tuple (Opcode, Time (int64), Other data...) . In case of properties that can be directly changed and affect game also need to be converted into property method so that the hook condition can be called. For example, in case of agents, the current_node_id is a property. The agent interface as well as the agent implementation need to be changed to include the property current_node_id as follows:
class IAgent:
....
@property
@abstractmethod
def current_node_id(self) -> int:
passclass Agent(IAgent):
...
@property
def current_node_id(self) -> int
return self._current_node_id
@current_node_id.setter
def current_node_id(self, value) -> None
if self.ctx.record.record():
# Do write for setting
self._current_node_id = valueSimilar changes for prev_node. In order to avoid roundabout, it will be good to avoid properties in general from now on in future development as much as possible.
NoOp Implementations
NoOp implementations need to be made for agents and sensors. These implementations do not actually update things by themselves but rely manual updates from a recording. The NoOp implementations should also have the recording hooks which should produce the same IR as that produced by functional versions. This ensures that the IR is not stateful in any manner and all information required is a part of the IR production process.
The only change in running replays with NoOp implementations and actual implementations is the extra information that might get calculated during the game that is not required to reconstruct the game itself.
Tests and Checks
This module will require tests to make sure everything is working correctly. Two types of checks are required:
- On each interface call, there is a proper write call
- Tests based on expected behaviors
Expected Changes
It is expected that there will be small changes across various parts of the codebase. While minor PRs should not affect the overall development process, it will still be better to merge any small PRs to dev before.