Shared Logic

 

Daniil Korostelev

(Forge of Empires)

Problems to solve:

Code duplication

  • Backend, frontend(s!), admin tool, other tools
  • A lot of effort to implement
  • Lots of code to maintain
  • Much space for mistakes

Problems to solve:

Unclean code separation

  • Game logic intertwined with:
    • Database
    • User Interface
    • Networking
    • etc.

Problems to solve:

Complicated data layout

  • No "player state" structure
  • Hard to understand
  • Hard to synchronize

Problems to solve:

Not fun to test

  • A lot to setup
  • Duplicated tests

Good game code

  • Domain-driven
  • Self-contained
  • Reusable
  • No boilerplate

Good game code:

Example (logic)

// data structure 
class Player {
    var name:String;
}

// logic entry point
class Commands {
    // action
    function changeName(newName) {
        validateName(newName); // no swear words!
        data.player.name = newName; // state change
    }
}

Good game code:

Example (front-end)

// listen to changes
// (could also be "reactive" property or something)
logic.data.player.onNameChanged(
    newName -> playerNameLabel.text = newName
);

// handle UI
function onNameEntered(input) {
    // execute action
    logic.commands.changeName(input)
}

Good game code:

Example (back-end)

function processCommand(name, args, checksum) {
    // do the preparations
    loadPlayerData();
    startTransaction();
	
    // call logic
    var changes = logic.execute(name, args);
	
    // consistency check
    if (changes.checksum != checksum)
        throw "invalid checksum!";
		
    // save to database
    commitChanges(changes);
}

Implementation:

goals

  • only write code that matters
  • one declaration per structure
  • one declaration per action
  • easy to reason about and change

Implementation:

the trick

Code generation for all the things!

  • change-tracking setters
  • reactive read-only data views
  • command dispatcher API
  • static and player data validation

Implementation:

tech

  • Powerful statically typed language
  • Compiler API or procedural macros
  • Still simple enough to iterate
  • Haxe \o/
  • ...or your fav tech (to some extent)

Typical issues

Cheating

  • Cheaters are real!
  • Shared logic helps
  • Shared logic hurts

Preventing cheating

  • Minimize command arguments
    • build(id:"house", x:1, y:2)
  • Check that commands are in order
  • Sync time between client and server
  • Be careful with random seeds

Random seed exploits

  • Global random seed is handy
  • ...but exploitable:
    • block connection
    • check the result
    • don't like the result, reload game
    • change the seed by some other action
    • try again
  • Two ways to fix:
    • Local seeds (can see, cannot affect)
    • Server-only random actions

Server commands

  • (Mostly) like normal commands
  • Executed only on the back-end
  • Changes are returned to the front-end
  • Have access to additional server APIs
  • Useful for: tournaments, matchmaking, payment, etc.

Exchangeables

  • "Messages" with id and payload
  • Sent by both game logic and admin tools
  • Handled by the game logic
  • Useful for: news, gifts, mail, async player interactions

Queries

  • Expose calculations
  • Must not modify state
  • Mostly for front-end

(Almost) true multiplayer:

tricky

  • Shared state between players
  • Potentially outdated state
  • Cannot be used for normal game logic
  • Only safe to use for display and UI confirmations

(Almost) true multiplayer:

server commands

  • Server commands API
  • Shared state should be locked
  • Changes dispatched to all participants
  • Commands should be more tolerant to invalid input
    • ignore the duplicate action if possible
    • return meaningful error if not

Back-end implementation

  • Storing JSON in Postgres tables worked for us
  • Player state is stored as snapshot + changes
  • Changes applied to the snapshot periodically
  • Some data is mapped to DB indexed columns

State migrations

  • needs lower-level data access
    • easy with JSON
    • possible anyway
  • can be lazy (only migrate when requested)
  • automatic detection if migration is needed
    • compare schemas
    • detect references to static data

Further exploration

  • Multiple data "roots"
  • Multiple logic modules
  • "Real" multi-player framework?

Thank you!

Shared Logic

By Dan Korostelev

Shared Logic

  • 1,070