@brandonhorst/yourturn@0.12.1
yourturn
yourturn is an opinionated framework for building turn-based multiplayer
browser games using Typescript.
You design the game logic as a state machine, design your UI as a function of
that state, and yourturn handles the rest.
Games built with yourturn get automatic support for:
- Networking
- Persistence
- Matchmaking
- Observation
yourturn includes support for games that require:
- Hidden Information
- Simultaneous or Out-of-turn Play
- Randomness
- Timers
yourturn is built to run on Deno and uses Deno KV as its database. Games
render their UI with Preact components.
Usage
To get started, it's recommended to start with yourturn-template.
If you want to use the library directly, it's available at jsr:@brandonhorst/yourturn.
Designing Game and UI Logic
These are the things you need to define to build your game.
Config
export type Config
This represents the necessary setup information for the game, such as number of players, decks, etc.
GameState
export type GameState
This represents complete state of the game at any given point in time, including information hidden from all players. This must be a datatype compatible with the the structured clone algorithm.
Move
export type Move
Any JSON-serializable object. This represents any action that any player takes. This can be thought of as the edges of the game's state machine's graph. Often, typescript "Union Types" are a good way to implement this.
PlayerState and ObserverState
export type PlayerState export type ObserverState
Two JSON-serializable objects. These are sent to users' browsers to be rendered
by the View. For games with hidden information, this should only contain
information that each player (and observers) should be privy to. In games with
no hidden information, GameState, PlayerState, and ObserverState can (but
do not need to be) the same type.
game
export const game: { setup(c: Config, o: { timestamp: Date }): GameState; isValidMove( s: GameState, move: Move o: { timestamp: Date; playerId: number }, ): boolean; processMove( s: GameState, o: { m: Move, timestamp: Date; playerId: number }, ): void; refreshTimeout?( s: GameState, o: { config: Config; timestamp: Date }, ): number | undefined; refresh?( s: GameState, o: { config: Config; timestamp: Date }, ): GameState; playerState(s: GameState, o: { playerId: number; isComplete: boolean }): PlayerState; observerState(s: GameState, o: { isComplete: boolean }): ObserverState; isComplete(s: GameState): boolean; };
An object with functions which determine the Game's behavior. These functions are only executed on the server.
setupis used to create the initialGameStateobject.isValidMoveis used to determine if aMovesent from the client is legimate. Ideally, the UI should prevent sending invalid moves, but this serves as a server-side failsafe.processMoveshould return a modified version of the providedGameStateaccording to the providedMove. This will only be called ifisValidMovereturned true. It is recommended to use a library likenpm:immerto make returning a mutated version ofGameStateeasier. This will potentially be called many times, and must efficient. It should be determistic and cannot access the network or the file system.playerStateshould create aPlayerStateobject to be sent to the client. This can be used to hide information from players, and to provide a nicer interface for building the UI upon.observerStateshould create anObserverStateobject to be sent to the client. This can be used to hide information from observers, and to provide a nicer interface for building the UI upon.isCompleteshould return true if the game is done and no furtherMoves should be permitted.refreshTimeoutcan be called to trigger arefreshcall and anisCompletecheck after a certain number of milliseconds. This can be used to implement timers.refreshTimeoutcan be used to create a newGameStateobject in response to arefreshTimeouttrigger. Note that it's OK to implementrefreshTimeoutwithout implementingrefreshin cases where the only time-based effect is ending the game in a loss (such as a chess timer).
View
export function PlayerView(props: { playerState: PlayerState; playerId: number; perform: (move: Move) => void; }); export function ObserverView(props: { observerState: ObserverState });
Two Preact components to define your views.
PlayerViewtakes aPlayerStatefor rendering the game for each player, as well as aplayerId. In response to user action, it can callperformto pass aMoveto the server and modify the gamestate.ObserverViewtakes anObserverStatefor rendering the game for people watching. It cannot perform actions.
In both cases, game state modifications cause a new state to be generated and the component to be re-rendered.