V4 Move Processor prototype

I mentioned in the most recent development update that I’m working on a move processor prototype, and I wanted to give a brief overview of what that is.

One can divide a game into the state and the visual representation of the state. The game state consists of such things as the identities, locations, and properties of the pieces. The visual representation of the state is the what you see on your table or on your screen. The game state is what matters formally, for playing the game according to the rules. The visual representation is for humans (players and spectators), to aid in interacting with the game state.

A system handling changes to the game state only need not be concerned with the visual representation at all. This is helpful, as visual representations—images—are frequently large in comparison with state data. A small piece image can be several KB of compressed data, while the representation of the same piece in the game state is unlikely to be more than a few hundred bytes. Our existing game server works this way. Only state changes are transmitted, no image data. It’s an efficient design, and one which I intend to keep.

What is in the game state—for any game? The games we’re interested in are on paper and turn-based. Some properties of what we need to represent come from the physicality of the cardboard, such as the location of pieces with respect to each other, how the pieces are stacked, which sides are up, how the pieces are oriented; other properties come from what is printed on the cardboard, such as unit types, strengths, movement allowances, etc. The properties which are due to the pieces being physical objects are also the ones which affect their visual representation; this is not a coincidence.

The possible state changes make a short list: create, destroy, clone, move, splice, flip, rotate, set property, unset property. Here “object” refers to a game object, such as a piece, a card, or a map.

  • create: Create an object
  • destroy: Destroy an object
  • clone: Make a copy of an object from another object
  • move: Move an object from one location to another
  • splice: Move an object out of or into a stack
  • flip: Turn an object over to a differnent side
  • rotate: Rotate an object about its center
  • set property: Set a named property with a given value on an object
  • unset property: Remove a named property from an object

(The difference between move and spice is that in a move, all objects stacked above the moving object remain in place above the moving object, whereas with splice, the moving object is removed from any stack it is in and inserted into another stack. Move and splice could be handled jointly; for technical reasons, it may be more congenial to treat them separately.)

These state changes are sufficient for handling the moves in any printed game. A move processor—a program which can take representations of these state changes as input and use them to update the game state—is an essential component for V4, and is what I’m prototyping now.

2 Likes

Thanks for saying this.

My mental model of vassal has long been that it holds, represents and stores game states, and gives the viewer ways to manipulate the state of a game. So thanks for confirming this.

Consider the states such as selected, or query. For example, the front end may make a query to the back-end as to which actions are permissible, or which moves are allowed, in the current game, piece, … state.

Yours,
Christian

Those are neither part of the game state nor state changes.

I’m not saying they’re unimportant, only that they’re not relevant for a move processor.

In some sense, the client shouldn’t have specific representations of flip, rotate, destroy, clone, etc. Instead, the client queries the back-end for the current state of the piece, which then replies with a JSON object. It is then up to the client to reflect that state to the user.

For example, a piece may have a Rotatable “trait”. Both the back-end and the client will know that the piece has that trait. However, the back-end will have code that sets, checks, etc. the current rotation, while the client will have code that can render the piece rotated. When the client requests the state of the piece, then the back-end will reply with a JSON object that contains something along the lines of

{ 
  ...
  "states": [
     {
         "trait-type": "Rotatable", 
         "name": "Rotation",
         "angle": 60, 
      },
     ...
  ],
  ...
}

The client Rotatable code will see this and render the piece rotated by 60°.

In that way, a module developer can supply back-end and client code for new traits. For example, a trait could be Movable, and on a query to the back-end, the back-end code will calculate possible moves and return those, taking the game specific rules into account. The client can then visualise those possible moves.

In fact, I think the protocol between client and back-end should be fairly simple:

  • A trait can specify that when the piece is selected, then a query should be sent to the back-end. For example, the above Movable trait could send the query can-i-move, and the back-end determines it can, then it sends back a reply which the client Movable code then visualises. A similar thing could happen for an Attacker trait.
  • A trait can specify certain actions. For example, if the Moveable trait was told that it can be moved, then an action could be move with parameters such as source and destination. The back-end can then decide the new state of the piece and reply with that, as well as some flag that says whether the action was completed or not. The client Movable code should then visualise the result of the action (including, for example, an error dialog).

The core code can provide simple traits, for example AlwaysMoveable as well as stubs that can be fleshed out by the module developer - say CheckedMovable.

Of course, most queries should only have responses that contain the needed or updated states - not necessarily the full state. Likewise, a Moveable trait need not reply with all possible paths when asked for legal moves, but rather a list of possible destinations.

Yours,
Christian

What I’m getting from this is that you’re claiming that the client shouldn’t have any verbs to send to the server. Without that, the client would have no way to effect changes to the game state, so I’m sure I’m misunderstanding what you’re saying and it won’t be fruitful to comment further before I do understand.

No, that’s not what I meant. Let me try to explain.

The client it self has two verbs:

  • query: A request to the back-end for information. The state of the game will not be changed by that request - it will provide information for the client to render certain things.
  • action: A request to the back-end to perform some action. Thus can change the state of the game, and the back-end will always give back a status code, such as accepted or declined.

Now, that seems rather limited, and indeed it would be if that was the only thing going.

If we consider that pieces (I’m staying with the current Vassal lingo here) - and perhaps other game components - have traits (again, current Vassal lingo - could also be though of as capabilities), then those traits will have one representation in the client, and another in the back-end.

Traits then formulate their own “verbs” - or sub-verbs to the client verbs query and action.

The query sub-verb would come into play when a piece with the particular trait needs some information in the client.

For example,

  • When a piece is selected in the client, then the client asks the piece to form a query of all its traits.
  • The client then ships that off to the back-end
  • The back-end delegates the query to its representation of the piece.
  • The back-end piece delegates the sub-queries to its back-end representation of its traits.
  • Each trait then formulates an answer to the query
  • The piece collects the answers, and hands that off to the back-end
  • The back-end then ships back the answers to the client(s)
  • The client gives the answers to its representation of the piece
  • The client piece delegates the answers to its traits.

An action sub-verb is more selective, in that it typically will involve a single trait.

For example,

  • An action is chosen from the pieces context menu
  • That action is tied to a specific trait, which sees that the action is performed
  • The trait then formulates a request, and asks the client piece to do that action
  • The client piece asks the client to send the action request to the back-end
  • The client ships off the request to the back-end
  • The back-end gets the request and delegates it to its representation of the piece
  • The back-end piece delegates the request to its back-end representation of the trait
  • The trait checks if the action is valid, and if so performs it, and thereby changes the state of the piece. It can formulate an accept reply that encodes that state change. If the action isn’t valid, then it formulates a decline reply, possibly with some reasoning attached.
  • The reply from the trait is handed off to the back-end piece
  • The back-end piece gives the reply to the back-end, which then ships that off to the client(s)
  • The client(s) receive the reply, and then delegates that to its representation of the piece
  • The client piece then hands the reply to its trait
  • The client trait then does the necessary updates in the client.

Let us take an example. Suppose we have the trait Movable. On the client it’s code could look like

from vassal.client import Trait
from vassal.client.trait import Query, Action

class Movable(Trait):
    def form_query(*args,**kwargs): 
        ...
        return Query('possible-destinations',...)
    def process_query_response(response):
        ...
    def form_action(*args,**kwargs):
        ...
        return Action('move',source,destination,...)
    def process_action_reponse(response):
        ...

On the back-end, the trait could look like

from vassal.backend import Trait
from vassal.backend.trait import Information, Result

class Movable(Backend): 
    def query_response(query,*args,**kwargs):
       ....
       if not Game().is_in_movement_phase():
           return None
       ,,,,
       return Information(possible_destinations)
    def action_response(action,*args,**kwargs):
        check = Game().is_move_legal(action.source, action.destination, piece):
        if check.reject:
            return Result(False, check.reason)
        ....
        return Result(True, new_state)

When a piece is selected, then the Movable trait will formulate a query to get the possible moves of that piece. The back-end responds with some information, which could be None.

When a piece is moved in the client, then the Movable trait will formulate an action to move that piece. The back-end then checks if the move is legal, and if it is, updates the state of the piece. The back-end then replies with the new state of the piece. If the move isn’t legal, then the back-end gives back a response which states why the move wasn’t legal, so that the client may display that to the user.

On query, an Attacker trait could ask the back-end for possible targets. On action, the Attacker trait could initially declare a combat, secondly ask for a resolution of that combat. Note, when a combat is resolved it may entail waiting for the users to implement the result. Thus, the back-end may need to enter a state where only certain actions (for example a retreat) are allowed.

In terms of logging, only action verbs need to be logged. query verbs are specific to the session, and will not need to be logged. Also, since an action may change state, it means that the result would likely need to be pushed to all clients.

I hope this makes it a bit clear what I had in mind.

The point of this, is that traits is what defines what is possible, and that traits can be as complex as the module developer would like them to be. A module developer could for example provide a factory of traits a la

class TraitFactory:
    def make_client_trait(name,*args,**kwargs):
        if name == 'MyMovable': 
            return mymodule.client.traits.MyMovable(*args,**kwargs)
    def make_backend_trait(name,*args,**kwargs):
        if name == 'MyMovable':
            return mymodule.backend.traits.MyMovable(*args,**kwargs)

Both the client and back-end will, when it reads in the piece definitions - say

{
  "pieces": [
    "a": {
       "traits": {
          "movable": {
            "type": "MyMovable",
            "parameters": {
              "allowance": 4,
              " mobility": "tracked"
            }
          },
          ...
        },
        ...
      },
      ...
    ],
  ...
}

ask known trait factories to create their representation of the trait.

Thus, a module developer can encode the games rules into the traits (in conjunction with some game instance).

Vassal can of course provide some basic traits which does very little, but which can form the basis (base class) for more elaborate traits.

Yours,
Christian