Hi everyone! I’m looking for some general help with a problem I am having in an application I am developing, I’ve been using OCaml for the last 5 years in my spare time, so I know a fair but about the basic language but sometimes struggle with the more advanced concepts like GDATs
I am developing a multiplayer card game using OCaml as a server, communicating with a web client via websockets using JSON as the interchange format.
Each card has an associated ability when used that can arbitrarily change the state of the game (currently Game.t
), some cards can take “arguments” that are chosen by the user (who to use the ability on, for example). I envisage a function similar with the following type to take this action: val make_card_action : Game.t -> Player.t -> arguments -> Game.t
, where Player.t
is the player making the action. arguments
would be some type defined somewhere for each card and another function would parse JSON into this type: val arguments_of_json : Yojson.t -> arguments option
.
Additionally the cards present in the game are chosen before the game starts, so these must be stored in a set of some kind (in my current attempt I create a set using Set.Make(Card)
)
So my question is quite open really - what is the best way to structure the cards, I have thought about using one module to represent a card (Card.t
), which could contain the functions above, eg:
module Card = struct
type t = {
name: string;
description: string;
make_action: Game.t -> Player.t -> Yojson.t -> Game.t;
} [@@deriving sexp]
let create
~description
~make_action
name = {
name; description; make_action
}
let name t = t.name
let description t = t.description
let compare t1 t2 = String.compare t1.name t2.name
let json_of_card { name; description; gives_money; is_required; min_players; } : Yojson.t =
(* Converts to a JSON format *)
let make_card_action t = t.make_action
end
module CardSet = Set.Make(Card)
(* ... make some cards ... *)
let all_cards = [card1; card2; card3]
A disadvantage of this approach is that I can’t think of a way to type arguments
ahead of time, not a deal breaker but I would like to do this to make it clearer how to add new cards.
My second idea was to use a combination of functors and first class modules:
module type Card_definition = sig
val name: string
val description: string
type arguments
val arguments_of_json : Yojson.t -> arguments
val make_action : Game.t -> Player.t -> arguments -> Game.t
end
module type Card = sig
val name: string
val description: string
val json_of_card: Yojson.t
type arguments
val arguments_of_json : Yojson.t -> arguments
val make_action : arguments -> unit
end
module Make_card(Definition : Card_definition): Card = struct
let name = Definition.name
let description = Definition.description
let json_of_card : Yojson.t = (* Card JSON format *)
type arguments = Definition.arguments
let arguments_of_json = Definition.arguments_of_json
let make_action = Definition.make_action
end
module CardSet = Set.Make(Card) (* Error: Unbound module Card - I don't know if something like this is possible as it's only a module type *)
(* ... make some cards ... *)
let all_cards = [(module card1); (module card2); (module card3)]
The advantage of this is I could store all my cards as individual modules and create more easily, however I can’t think of a way to make it work with a Set
My last thought was some combination of the above, something like this (obviously this won’t work but it’s demonstrating my thinking):
module type Card_definition = sig
val name: string
val description: string
type arguments
val arguments_of_json : Yojson.t -> arguments
val make_action : arguments -> unit
end
module type Card = sig
type t
type arguments
val compare: t -> t -> int
val name: t -> string
val description: t -> string
val json_of_card: t -> Yojson.t
val arguments_of_json: Yojson.t -> arguments
val make_action: arguments -> unit
end
module type Card_instance = sig
module Card: Card
val singleton: Card.t
end
module Make_card(Definition : Card_definition): Card_instance = struct
module Card = (struct
type t = {
name: string;
description: string;
}
type arguments = Definition.arguments
let name t = t.name
let description t = t.description
let compare t1 t2 = String.compare t1.name t2.name
let json_of_card t : Yojson.t = `Null
let arguments_of_json = Definition.arguments_of_json
let make_action = Definition.make_action
end : Card)
let singleton : Card.t = {
name: Definition.name;
description: Definition.description;
}
end
module CardSet = Set.Make(Card)
(* ... make some cards ... *)
let all_cards = [(module card1); (module card2); (module card3)]
I’m not sure what the best way to approach this problem is, my feeling is there is some feature of the language I can use to achieve this (FC modules, functors, open types, GADTs),
but I’ve not got much experience using any of these so any advice on how to approach this would be very much appreciated. Any questions or if something is not clear let me know and I can try and vie more detail. Thanks a lot!