1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
open Core

type t = {
  board : Board.t;
  players : Player.t list;
  current_player : int;
  tile_bag : Tile_bag.t;
  end_state : End_state.t option;
  turn : int;
  consecutive_passes : int;
}
[@@deriving to_yojson]

let create_initial_game ?rng player_names =
  let rec build_players idx names bag acc =
    match names with
    | [] -> (List.rev acc, bag)
    | name :: rest ->
        let player = Player.make idx name in
        let rack, remaining = Tile_bag.fill_rack (Player.get_rack player) bag in
        let player = Player.set_rack player rack in
        build_players (idx + 1) rest remaining (player :: acc)
  in
  let bag = Tile_bag.create_tile_bag ?rng () in
  let players, bag = build_players 0 player_names bag [] in
  {
    board = Board.create_empty_board ();
    players;
    current_player = 0;
    tile_bag = bag;
    end_state = None;
    turn = 1;
    consecutive_passes = 0;
  }

let find_winner players =
  List.fold players ~init:(List.hd_exn players) ~f:(fun best p ->
      if Player.get_score p > Player.get_score best then p else best)

let calculate_margin players winner =
  let others =
    List.filter players ~f:(fun p -> Player.get_id p <> Player.get_id winner)
  in
  let second =
    List.fold others ~init:(List.hd_exn others) ~f:(fun best p ->
        if Player.get_score p > Player.get_score best then p else best)
  in
  Player.get_score winner - Player.get_score second

let apply_final_scoring players player_went_out =
  let total_remaining =
    List.fold players ~init:0 ~f:(fun sum p ->
        sum + Rack.get_total_points (Player.get_rack p))
  in
  players
  |> List.map ~f:(fun p ->
         let deducted =
           Player.get_score p - Rack.get_total_points (Player.get_rack p)
         in
         if Player.get_id p = Player.get_id player_went_out then
           Player.set_score p (deducted + total_remaining)
         else Player.set_score p deducted)

let check_game_end state last_player tiles_placed =
  if tiles_placed = 0 then None
  else if
    Rack.is_empty (Player.get_rack last_player)
    && Tile_bag.is_empty state.tile_bag
  then
    let adjusted_players = apply_final_scoring state.players last_player in
    let winner = find_winner adjusted_players in
    Some
      End_state.
        {
          winner;
          margin = calculate_margin adjusted_players winner;
          reason = Rack_out last_player;
        }
  else None

let update_players players idx updated =
  List.mapi players ~f:(fun i p -> if i = idx then updated else p)

let get_current_player state = List.nth_exn state.players state.current_player
let is_game_over state = Option.is_some state.end_state