Skip to content
Alex Komoroske edited this page Jan 19, 2017 · 18 revisions

This page is just a bunch of scratch notes from an early exploration. It's superceded by the other documents in the wiki.

A server written in Go that works on heroku,using a generic SQL datastore.

The model mutation logic is written in Go, and compiled to JS with gopher-js. Client-side, items are custom elements (so creation of new one goes through a parametized constructor), and then they're moved around as logical items based on mutators. Additional logic to put the custom elements in the right containers is necessary to write.

The server is canonical. There is a queue of moves to apply, which are mutations to game state. Each move has a IsLegal that must return true, and an Apply/Undo that does (or undoes) the gamestate. (That might be hard to do given that models aren't in memory).

Moves are proposed clientside and go into waiting state. Different custom elements might be different things based on this--how likely it is to fail, etc.

When a move is made, the model increments the version count, and clients download the moves necessary to apply to their model state.

The canonical model state is transformed when transfered to the client based on certain rules of who can see what (maybe you can see all of your own cards, but only the count of anothers, or not even that).

We have to reason about game states when multiple people can make moves in a round, and we leave that round when a certain thing is true (time passes, or a move is made where now the Round says it's done). We also need to reason about time to animate--is dealing a set of three cards one action, or multiple actions? How do you get everything to wait while they animate?

The way to get them to wait while you animate is that there is a queue of moves to apply. The server says when there are more to fetch, and you get them and bbring them to the clientside queue. You then apply them in order, and some of them have waiting (meanwhile other moves might have happened and you're fetching them into the queue). When you make a queue it goes into the waiting queue clientside (and waiting state, which might differ for different moves). When the server says here's a new move and we realize it's one of ours (in waiting), we remove the item from waiting and either commit or reject it.

Will likely want to investigate operational transform. Investigated it, and I don't think it works, because fundamentally no client knows which moves to apply when.

How actually to do an object hierarchy in Go that is reasonable and factors out a reasonable amount of logic is unclear. Will likely rely on reflection and struct tags.

type User struct {
  //0 is reserved for GameMaster
  Index int
  DisplayName string
}

type GameState struct {
  CurrentUser int
  UserStates []interface{}
}

type PopFourGameState struct {
  GameState
}

func (f *PopFourGameState) GameMasterState() *PopFourGameMasterState {
 //call f.GameState.StateForUser(0).(*PopFourGameMasterState)
}

type JSONUserState interface {
  //The full JSON State represented by this object
  JSON() []byte
  //The JSON that has been sanitized: fields omitted, converted to just count, items with data replaced with just a unique string (e.g. cards in a users hand where others can see when they are reordered but not what they contain)
  SanitizedJSON() []byte
}

//A utility function takes an interface{} that implements JSON, then for each property uses reflection to read back struct tags about how to summarize it and then applies that transformation.


type GameMove interface {
  IsLegal(state interface{})
  //It's kind of unfortunate that we need to have all of this type conversion in each method in order to satisfy the generalized interface definitions...
  Apply(state interface{})
}

Notion of Decks and Stacks.

Decks are the set of all types of card. There are static for a given game, in the config file. (What about things like Cards Against Humanity where there are write-in cards?). Decks are broken into Stacks, which are ordered subsets of cards from that deck. The union of all Stacks from a given deck must be the entire deck, and moving cards between stacks should be done with well-tested helper methods to ensure this invariant is held. Stacks are owned by players--either GameMaster, or individual players, and the stack visibility is controlled based on the normal sanitizing properties of UserStates. (one hard thing is if there is a stack in a user's hand where some are visible to others and some arent, and they're interleaved)

type UserState interface {
  PropertyNames() []string  
  GetProperty(name string) {}interface
  SetProperty(name string, val {}interface) error
  StackPropertyNames() []string (list of all names that would give you something with GetStack()
  GetStack(name) Stack
  //These likely should be private (would that work across lib boundaries?) to enforce that you should use library methods to SwapCards position within stacks or MoveCards between stacks
  AddToStack(position int, Card)
  RemoveFromStack(position int) Card
  BoolPropertyNames() []string
  GetBool(name)
  SetBool(name string, val bool)
  //Repeat the last three for Int, Float, and String, probably something about DIce
  PropertyACL(name string) propertyACL
}

Some of these methods are just convenience, so maybe there's a general GetProperty/SetProperty/List Properties, and then convenience methods to convert to GetDeck or whatever.

UserStates could choose to store these things in specific slots in a struct (perhaps storing config in struct tags, or just in generic maps).

There is a static list of components -- cards, dice, meeples, resource blocks, etc. Everything that is not the board. Each component can have an owner, and there are stacks for them (although stacks are most useful in cards). The container (e.g. "bank") is the thing that will set their X/Y property, but that's not in the game state, it's derivable from the GameState plus clientside logic.

Every component is in a "deck": a collection of similar items, which can be shuffled. (THe deck terminology makes most sense for cards)

Compnents have:

  • Static, always public config. If someone can see that this component is in a given stack (which the stacks' sanitization settings control) then they can see all of this config information. (It is visible to whoever can see the front of the card, conceptually)
  • Public dynamic state. E.g. if for example it was semantically relevant which direction a meeple is pointing, it could be encoded here.
  • Private dynamic state: State that can change but should only be seen by its owner. e.g. the upward-facing number of a dice behind a screen in Dark Moon.

It might be cleaner to just have static and dynamic properties, the precise visibility of which is derived from the ACLs provided by its current owner.

The concept of 'deck' and 'stack' make more sense for cards, but the principles apply for all components:

  1. Anything that can be moved or owned is a component (anything that instructions to a game would list as included)
  2. Every component is a member of a 'deck'. A deck is an exhaustive list of a collection of similar items, in a stable reference ordering that never changes--it is set at config time and can be stably referred to at any time.
  3. A Stack is a ordered subset of a Deck. It consists of a reference to the deck, and then a list of int IDs to members of that deck, in sorted order.
  4. Every deck is split up into stacks who are owned by different players (in many cases, GameManagerUser, but in other cases specific users). The union of all items in those stacks must be precisely the contents of the whole Deck. That is, every item enumerated in the deck must be a member of precisely one stack, each of which is in one UserState object.
  5. All moves should be careful to maintain the invariant, via transactions, that each card/component must be in precisely one stack at a time.
  6. Components have a static state that is configured in the config and never changes (e.g. what is printed on the card). They also may have dynamic state that can change at runtime according to moves.
  7. The UserState object that owns a stack configures its visibility. Some stacks are fully visible to all users; some only reveal non-real stable IDs for cards (so we can keep track of animating order of the cards without revealing their contents) some reveal only the count of items, and some reveal nothing.
  8. The precise X/Y positions of a component are not encoded in the gamestate; that is derivable from the current gamestate and the client-side display logic.

(Thought exercise: any examples where a static bit of state for a component is public, but the dynamic state should be private to other users? Or vice versa?)

Clone this wiki locally