Skip to content

GLIR: Gamms Game Record #24

@bridgesign

Description

@bridgesign

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
    """
        pass

The 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:
         pass
class 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 = value

Similar 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.

Sub-issues

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions