user1198582
user1198582

Reputation:

Functional Banana Traveller - Ship System

In this game, I want to have Player Classes, Player Levels, and ships one may have according to class and level. You can only have one ship at a time. I have come to the conclusion that what I need are datakinds and data families.

Here's a snippet of what I have, followed by my questions.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses}
{-# LANGUAGE ConstraintKinds #-}

data PlayerClass = Pirate | Merchant | SpaceNinja deriving Show
data ShipClass = Tiny | Medium | Large deriving Show
data Merchant = Rookie | Swindler | IndustryCaptain deriving Show
data Pirate   = Lubber | Matey | Captain deriving Show

data family PShip (a :: PlayerClass) (b :: ShipClass)
data instance PShip Pirate Tiny = Junk
  { _hull_junk              :: Hull
  , _bridge_junk            :: Bridge
  , _cargo_compartment_junk :: CargoCompartment
  , _fuel_tank_junk         :: Tank
} deriving Show

My understanding is that with DataKinds, I can promote Terms to Types, but still use them as Terms. I want a class to abstract the implementation of operations for PShips. I would like to constrain at the kind level (PlayerClass), and then create instances for types with that kind. Here's a snippet of what I am trying to do, but can't.

 class (PShip a) => Ship a (b :: PlayerClass) where
  ...
 instance Ship (Pirate Tiny) Lubber ...

Does this make sense? Am I on the right track, or should I back up and re-think this problem? Have I articulated the problem clearly?

Upvotes: 2

Views: 88

Answers (1)

Cirdec
Cirdec

Reputation: 24166

Don't enforce the constraint on "ships one may have according to class and level" at the type level. It may be OP, but there's (probably) no reason the game shouldn't work with a level 1 space ninja captaining a large 19th century ship of the line. Instead of resorting to type-level programming, the rules of the game can be written as ordinary functions, or even encoded as data.

If you want to pick which ship someone is using from their class and ship size, you can just write a function.

data Ship = Ship {
    _hull_junk              :: Hull,
    _bridge_junk            :: Bridge,
    _cargo_compartment_junk :: CargoCompartment,
    _fuel_tank_junk         :: Tank
}

data Hull = Raft | Sloop | Brig | Schooner | Starfighter | ...

playerShip :: PlayerClass -> ShipClass -> Ship
playerShip Pirate Tiny = Ship {
    _hull_junk = Sloop,
    _bridge_junk = ...
}
playerShip SpaceNinja Tiny = Ship {
    _hull_junk = Starfighter,
    _bridge_junk = ...
}
...

We've moved the rules about which ship goes with a player and ship class from the type level to the level of ordinary Haskell code.

You specified that there are multiple ships one might have by class and level. To handle that we'd return a list of ships based on the player's class and the ship size

allowedShips :: PlayerClass -> ShipClass -> [Ship]

To implement this easily, we might make a game data file that includes a list of ships, and for each ship, a list of which player classes and ship classes it can be used for. By doing so, we move the rules about which ships can go with a player and ship class from the level of ordinary Haskell code to the level of data.

[
    ([(Pirate, Tiny)], Ship {
        _hull_junk = Sloop,
        _bridge_junk = ...
    }),
    ([(SpaceNinja, Tiny)], Ship {
        _hull_junk = Sloop,
        _bridge_junk = ...
    }),
    ([(Pirate, Tiny),(Merchant, Tiny),(SpaceNinja,Tiny)], Ship {
        _hull_junk = Raft,
        _bridge_junk = ...
    }),
    ...
]

To start with, you can list data like this in a .hs file shipData :: [([(PirateClass, ShipClass)],Ship); shipData = ... and implement allowedShips by filtering the shipData by pirate class and ship class.

If all of the _junk is data, we can go further and treat this data as data, storing it in a file and reading it with derived Read instances.

Upvotes: 1

Related Questions