A two player, simultaneous turn desktop strategy game.
See docs/install.md.
Core module layout:
Located at ./src/Game. Completely UI-agnostic, could be broken into its own separate package if we wanted. Imports nothing local outside of Game.*
.
Everything in ./src outside of ./src/Game/*
.
Tracks local state like what base the user has selected. Uses the server to exchange orders with the opponent. When both players have moved uses the game rules to step the game forward.
A local package located at ./json-relay. Provides an executable server which allows clients to join rooms and relays JSON messages between clients in the same room. Knows nothing about this specific game.
The UI uses a Model/View/Update architecture. The game rules also have a Model and Update, but no View.
This can be summarized with a few type signatures.
Game rules:
-- in Game.Model
data Model = Model
{ modelPlaces :: HashMap PlaceId Place
...
}
-- in Game.Update
update :: HashMap Player Orders -> Model -> Model
Gloss UI:
-- in Model, with Game.Model imported as Game
data Model = Model
{ modelGame :: Game.Model
, modelSelection :: Selection
...
}
-- in View
view :: Model -> Picture
-- in Update
update :: Input -> Model -> Model
In Gloss unlike Elm there's no Msg
type. That leaves it up to us to figure out how to get the View and Update agreeing on where clickable things are displayed without drowning in duplicate code.
Our solution is the Layout module, which provides a description of where each clickable item is in the UI:
newtype Layout item
= Layout { unLayout :: [Set item] }
uiLayout :: Model -> Layout Item
This is used by the View to render the UI and by Update to process clicks.
Having many features or graphics isn't a goal of the game. So we can use simple tools and implement the rest of what we need ourselves.
We use Gloss which provides a keyboard/mouse input type, an image output type, and a MVU app runner. Since we implement everything else the code provides examples of panning, zooming, and mapping mouse clicks to UI items.
Multiplayer is synchronous.
Each player has a Game.Model. When both players have ended their turns, each calls Game.Update.update with the same inputs (both their and their opponent's orders). This rolls the game model forward to the start of the next turn.
This means Game.Update.update
must be deterministic. We avoid functions like Data.HashMap.Strict.toList
in the game code.
Each player also has a UI model: Model. These will have different values for each player.
There's no attempt to make the game resistant to bad actors. If someone wants to cheat they can modify their client to view board info that should be hidden.
For this particular project I wanted the satisfaction of doing the coding myself.
Fork and add your own twist!
-
Mitchell Rosen: for getting multiplayer working in The Depths, an earlier game this multiplayer implementation is based off of.
-
Gib Jeffries: for map development in Onshape and playtesting.