Start on gameplay actions, move modeling.
4 files changed, 216 insertions(+), 24 deletions(-)

M README.md
A => src/panacea_lib/gameplay.ml
M src/panacea_lib/gamestate.ml
A => src/panacea_lib/gamestate.mli
M README.md +6 -0
@@ 18,4 18,10 @@ I get it to play enough games I can use 
 - Model a decent gamestate, flow, render, REPL? Very bare minimum.
 - Start filling it in with proper cards, characters, actions.
 
+## High-level strategy
+
+- MAXIMIZE cure research
+- MINIMIZE outbreak potential
+- MINIMIZE outbreak damage
+
    [1]: https://git.sr.ht/~srpablo/ScrabbleCheat

          
A => src/panacea_lib/gameplay.ml +57 -0
@@ 0,0 1,57 @@ 
+open Gamestate
+
+(** 
+ * Do 4 actions.
+ * Draw 2 Player cards.
+ * Infect cities.
+ *)
+
+(** What is a "move"?
+ * Bot receive a Gamestate and generate a list of moves for it.
+ * AI will then look at them and the Gamestate and select the "best" one.
+ * Visibility into the decks is disallowed.
+ *)
+
+type action =
+  | Drive of infection_city          (* Move to a city connected to the one you are in. *)
+  | DirectFlight of infection_city   (* Discard a city card to move to the city named on the card.*)
+  | CharterFlight of infection_city  (* Discard the city card matching the city you are in to go to _any_ city *)
+  | ShuttleFlight of infection_city  (* Move from a city with a research station to another with a research station. *)
+  | BuildResearch of infection_city  (* Discard card matching city you're in, build a research station. *)
+  | TreatDisease of infection_city * infection_color  (* Remove cubes *)
+  | ShareKnowledge of infection_city (* Give card to another player *)
+  | DiscoverCure of infection_color * infection_card list
+
+
+type move = (action * action * action * action)
+
+
+let apply_action gs action =
+  let active_player = Gamestate.active_player gs in
+  match action with
+  | Drive city ->
+      gs
+      |> Gamestate.move_player_to ~player:active_player ~city:city
+      |> Gamestate.decrement_actions_remaining
+  | DirectFlight city ->
+      gs
+      |> Gamestate.move_player_to ~player:active_player ~city:city
+      |> Gamestate.discard_player_card ~player:active_player ~card:(City city)
+      |> Gamestate.decrement_actions_remaining
+  | CharterFlight city ->
+      gs
+      |> Gamestate.move_player_to ~player:active_player ~city:city
+      |> Gamestate.discard_player_card_at_location ~player:active_player
+      |> Gamestate.decrement_actions_remaining
+  | ShuttleFlight city ->
+      gs
+      |> Gamestate.move_player_to ~player:active_player ~city:city
+      |> Gamestate.decrement_actions_remaining
+  | BuildResearch city ->
+      gs
+      |> Gamestate.place_research_station ~city:city
+      |> Gamestate.decrement_actions_remaining
+  | _ -> gs
+
+
+let generate_moves _gs = [5]

          
M src/panacea_lib/gamestate.ml +91 -24
@@ 2,15 2,33 @@ open Base
 
 (**
  * The Gamestate contains everything you need to properly model a game in-progress.
+ * Also contains functions to alter or change it.
  *)
 
 type difficulty = 
-  | Introductory  (* 4 Epidemic cards *)
-  | Standard      (* 5 Epidemic cards *)
-  | Heroic        (* 6 Epidemic cards *)
+  | Introductory
+  | Standard
+  | Heroic
+
+
+type infection_color =
+  | Black
+  | Blue
+  | Red
+  | Yellow
   [@@deriving show]
 
 
+type infection_city = 
+  | Algiers | Atlanta | Baghdad | Bangkok | Beijing | Bogota | BuenosAires | Cairo
+  | Chennai | Chicago | Delhi | Essen | HoChiMinhCity | HongKong | Istanbul | Jakarta
+  | Johannesburg | Karachi | Khartoum | Kinshasa | Kolkata | Lagos | Lima | London
+  | LosAngeles | Madrid | Manila | MexicoCity | Miami | Milan | Montreal | Moscow
+  | Mumbai | NewYork | Osaka | Paris | Riyadh | SanFrancisco | Santiago | SaoPaolo
+  | Seoul | Shanghai | StPetersburg | Sydney | Taipei | Tehran | Tokyo | Washington
+  [@@deriving sexp, compare, show]
+
+
 type role = 
   (* - As an action, take any discarded Event card and store it on this card.
    * - When you play the stored Event card, remove it from the game.

          
@@ 50,21 68,7 @@ type role =
   | Scientist
   [@@deriving show]
 
-type infection_color =
-  | Black
-  | Blue
-  | Red
-  | Yellow
-  [@@deriving show]
 
-type infection_city = 
-  | Algiers | Atlanta | Baghdad | Bangkok | Beijing | Bogota | BuenosAires | Cairo
-  | Chennai | Chicago | Delhi | Essen | HoChiMinhCity | HongKong | Istanbul | Jakarta
-  | Johannesburg | Karachi | Khartoum | Kinshasa | Kolkata | Lagos | Lima | London
-  | LosAngeles | Madrid | Manila | MexicoCity | Miami | Milan | Montreal | Moscow
-  | Mumbai | NewYork | Osaka | Paris | Riyadh | SanFrancisco | Santiago | SaoPaolo
-  | Seoul | Shanghai | StPetersburg | Sydney | Taipei | Tehran | Tokyo | Washington
-  [@@deriving sexp, compare, show]
 
 type player_card_event = {
   name: string;

          
@@ 96,7 100,7 @@ type board_city = {
   num_yellow : int;
   has_research_station : bool;
   neighbors : infection_city list;
-} [@@deriving show]
+}
 
 
 (* This is a bit of a nasty workaround to use a custom ADT as a key for a Base.Map

          
@@ 137,6 141,7 @@ type gamestate = {
   num_outbreaks : int;
   curr_turn : int;
   actions_remaining : int;
+  infection_rate_marker_and_index : (int * int list);
   research_stations_remaining : int;
   infection_deck : infection_card list;
   infection_discard : infection_card list;

          
@@ 152,9 157,13 @@ type gamestate = {
   is_yellow_cured : bool;
 }
 
+type t = gamestate
+
+let newline_split = String.concat ~sep:"\n"
+let comma_split = String.concat ~sep:","
 
 let show_gamestate gs = 
-  let player_string = List.map ~f:show_player gs.players |> String.concat ~sep:"\n" in
+  let player_string = List.map ~f:show_player gs.players |> newline_split in
   let board_string = show_board gs.board in
   let turn_string = Printf.sprintf "Current Turn: %d. Number of outbreaks: %d. Actions remaining: %d. Research stations available: %d"
     gs.curr_turn gs.num_outbreaks gs.actions_remaining gs.research_stations_remaining

          
@@ 165,7 174,23 @@ let show_gamestate gs =
   let cure_string = Printf.sprintf "CURES: Red %B. Blue %B. Black %B. Yellow %B."
     gs.is_red_cured gs.is_blue_cured gs.is_black_cured gs.is_yellow_cured
   in
-  String.concat ~sep:"\n" ["===== GAMESTATE ====="; player_string; board_string; turn_string; cube_string; cure_string]
+  let infection_deck_str = (List.cons "Infection Deck:" @@ List.map gs.infection_deck ~f:show_infection_card) |> comma_split in
+  let infection_discard_str = (List.cons "Infection Discard:" @@ List.map gs.infection_discard ~f:show_infection_card) |> comma_split in
+  let player_deck_str = (List.cons "Player Deck:" @@ List.map gs.player_deck ~f:show_player_card) |> comma_split in
+  let player_discard_str = (List.cons "Player Discard:" @@ List.map gs.player_discard ~f:show_player_card) |> comma_split in
+  newline_split
+    [
+      "===== GAMESTATE =====";
+      player_string;
+      board_string;
+      turn_string;
+      cube_string;
+      cure_string;
+      infection_deck_str;
+      infection_discard_str;
+      player_deck_str;
+      player_discard_str;
+    ]
 
 
 let empty_board = 

          
@@ 272,6 297,7 @@ let empty_gamestate =
     board=empty_board;
     curr_turn=0;
     actions_remaining=4;
+    infection_rate_marker_and_index=(0, [2;2;2;3;3;4;4]);
     research_stations_remaining=6;
     infection_deck=starting_infection_deck;
     infection_discard=[];

          
@@ 378,17 404,16 @@ let place_outbreaks difficulty gs =
 
 
 (** Places the first research center in Atlanta. *)
-let place_research_station place gs =
+let place_research_station ~city gs =
   let {board=b; research_stations_remaining=r;_} = gs in
   let updated_board =
-    Map.change b place ~f:(fun x -> match x with
+    Map.change b city ~f:(fun x -> match x with
       | None -> None
       | Some b_city -> Some {b_city with has_research_station=true})
   in
   {gs with research_stations_remaining=r-1; board=updated_board}
 
 
-(** Gives us a Gamestate that's ready to play! *)
 let starting_game ~player_names ~difficulty = 
   let transformations = 
     [

          
@@ 398,10 423,52 @@ let starting_game ~player_names ~difficu
       hand_out_cards;
       infect_cities;
       place_outbreaks difficulty;
-      place_research_station Atlanta;
+      place_research_station ~city:Atlanta;
     ]
   in
   List.fold_left
     ~init:empty_gamestate
     ~f:(fun accum x -> x accum)
     transformations
+
+
+let update_location city player = {player with location=city} 
+
+let discard_card card player =
+  let new_hand = List.filter player.hand ~f:(fun x -> not (phys_equal x card)) in
+  {player with hand=new_hand} 
+
+let discard_location_card player =
+  let location_card = City player.location in
+  discard_card location_card player
+
+
+let update_player_named (name : string) (func : player -> player) : (player -> player) =
+  fun p ->
+    match String.equal p.name name with
+    | true -> func p
+    | false -> p
+
+
+let move_player_to ~player ~city gs =
+  let updated_players = List.map ~f:(update_player_named player (update_location city)) gs.players in
+  {gs with players=updated_players}
+
+
+let discard_player_card ~player ~card gs =
+  let updated_players = List.map ~f:(update_player_named player (discard_card card)) gs.players in
+  {gs with players=updated_players}
+
+
+let discard_player_card_at_location ~player gs =
+  let updated_players = List.map ~f:(update_player_named player discard_location_card) gs.players in
+  {gs with players=updated_players}
+
+
+
+let decrement_actions_remaining gs =
+  {gs with actions_remaining=gs.actions_remaining - 1}
+
+
+let active_player gs =
+  (List.nth_exn gs.players gs.curr_turn).name

          
A => src/panacea_lib/gamestate.mli +62 -0
@@ 0,0 1,62 @@ 
+type t
+
+type difficulty = 
+  | Introductory
+  | Standard
+  | Heroic
+
+
+type infection_city = 
+  | Algiers | Atlanta | Baghdad | Bangkok | Beijing | Bogota | BuenosAires | Cairo
+  | Chennai | Chicago | Delhi | Essen | HoChiMinhCity | HongKong | Istanbul | Jakarta
+  | Johannesburg | Karachi | Khartoum | Kinshasa | Kolkata | Lagos | Lima | London
+  | LosAngeles | Madrid | Manila | MexicoCity | Miami | Milan | Montreal | Moscow
+  | Mumbai | NewYork | Osaka | Paris | Riyadh | SanFrancisco | Santiago | SaoPaolo
+  | Seoul | Shanghai | StPetersburg | Sydney | Taipei | Tehran | Tokyo | Washington
+
+
+type infection_color =
+  | Black
+  | Blue
+  | Red
+  | Yellow
+
+
+type infection_card = infection_city
+
+
+type player_card_event = {
+  name: string;
+  description: string;
+}
+
+
+type player_card = 
+  | Event of player_card_event
+  | Epidemic
+  | City of infection_city
+
+
+(** Printable representation of a Gamestate. *)
+val show_gamestate : t -> string
+
+(** Gives us a Gamestate that's ready to play! *)
+val starting_game : player_names:string list -> difficulty:difficulty -> t
+
+(** Places the current active player to the location. *)
+val move_player_to : player:string -> city:infection_city -> t -> t
+
+(** Places the current active player to the location. *)
+val discard_player_card : player:string -> card:player_card -> t -> t
+
+(** Places the current active player to the location. *)
+val discard_player_card_at_location : player:string -> t -> t
+
+(** Use an action of this player's turn. *)
+val decrement_actions_remaining : t -> t
+
+(** Place a Research Station at this city. *)
+val place_research_station : city:infection_city -> t -> t
+
+(** Get the name of the player who's turn it is. *)
+val active_player : t -> string