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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
open Core
module StringMap = Map.Make (String)

type room_id = string [@@deriving yojson]

type waiting_player = {
  id : string;
  player_name : string;
  join_time : float;
  is_ai : bool;
}
[@@deriving yojson]

type waiting_room = {
  room_id : room_id;
  room_name : string;
  host_id : string;
  host_name : string;
  players : waiting_player list;
  created_at : float;
  max_players : int;
}
[@@deriving yojson { exn = true }]

type game_room = {
  room_id : room_id;
  game_state : Game_state.t;
  player_socket_ids : string list;
  started : bool;
}
[@@deriving to_yojson]

type rooms_state = {
  waiting_rooms : waiting_room StringMap.t;
  active_rooms : game_room StringMap.t;
  next_room_id : int;
}

type located_room =
  | Game_room of room_id * game_room
  | Waiting_room of room_id * waiting_room
  | No_room

let find_index pred lst =
  let rec aux idx = function
    | [] -> None
    | x :: xs -> if pred x then Some idx else aux (idx + 1) xs
  in
  aux 0 lst

let create_empty_rooms_state () =
  {
    waiting_rooms = StringMap.empty;
    active_rooms = StringMap.empty;
    next_room_id = 1;
  }

let create_waiting_room state host_id host_name ?room_name () =
  let room_id = Printf.sprintf "room-%d" state.next_room_id in
  let now = Core_unix.gettimeofday () in
  let player =
    { id = host_id; player_name = host_name; join_time = now; is_ai = false }
  in
  let room =
    {
      room_id;
      room_name = Option.value room_name ~default:(host_name ^ "'s Game");
      host_id;
      host_name;
      players = [ player ];
      created_at = now;
      max_players = 4;
    }
  in
  let waiting_rooms = Map.set state.waiting_rooms ~key:room_id ~data:room in
  ({ state with waiting_rooms; next_room_id = state.next_room_id + 1 }, room)

let join_waiting_room state room_id player_id player_name =
  match Map.find state.waiting_rooms room_id with
  | None -> (false, None, None, Some "Room not found")
  | Some room ->
      if List.length room.players >= room.max_players then
        (false, None, None, Some "Room is full")
      else if List.exists room.players ~f:(fun p -> String.equal p.id player_id)
      then (false, None, None, Some "Already in room")
      else
        let player =
          {
            id = player_id;
            player_name;
            join_time = Core_unix.gettimeofday ();
            is_ai = false;
          }
        in
        let updated_room = { room with players = room.players @ [ player ] } in
        let waiting_rooms =
          Map.set state.waiting_rooms ~key:room_id ~data:updated_room
        in
        let state = { state with waiting_rooms } in
        (true, Some state, Some updated_room, None)

let add_ai_player state room_id difficulty =
  match Map.find state.waiting_rooms room_id with
  | None -> (false, None, None, Some "Room not found")
  | Some room ->
      if List.length room.players >= room.max_players then
        (false, None, None, Some "Room is full")
      else
        let ai_count =
          List.fold room.players ~init:0 ~f:(fun acc p ->
              if p.is_ai then acc + 1 else acc)
        in
        let ai_id = Printf.sprintf "ai-%d" (ai_count + 1) in
        let name =
          "AI Bot ("
          ^ (match difficulty with
            | Ai.Easy -> "Easy"
            | Ai.Medium -> "Medium"
            | Ai.Hard -> "Hard")
          ^ ")"
        in
        let ai =
          {
            id = ai_id;
            player_name = name;
            join_time = Core_unix.gettimeofday ();
            is_ai = true;
          }
        in
        let updated_room = { room with players = room.players @ [ ai ] } in
        let waiting_rooms =
          Map.set state.waiting_rooms ~key:room_id ~data:updated_room
        in
        let state = { state with waiting_rooms } in
        (true, Some state, Some updated_room, None)

let start_game_from_waiting_room state room_id requesting_player_id =
  match Map.find state.waiting_rooms room_id with
  | None -> (false, None, None, Some "Waiting room not found")
  | Some room ->
      if not (String.equal room.host_id requesting_player_id) then
        (false, None, None, Some "Only host can start game")
      else if List.is_empty room.players then
        (false, None, None, Some "Need at least 1 player to start")
      else
        let player_names = List.map room.players ~f:(fun p -> p.player_name) in
        let game_state = Game_state.create_initial_game player_names in
        let game_room =
          {
            room_id;
            game_state;
            player_socket_ids = List.map room.players ~f:(fun p -> p.id);
            started = true;
          }
        in
        let waiting_rooms = Map.remove state.waiting_rooms room_id in
        let active_rooms =
          Map.set state.active_rooms ~key:room_id ~data:game_room
        in
        let state = { state with waiting_rooms; active_rooms } in
        (true, Some state, Some game_room, None)

let get_waiting_rooms state =
  Map.to_alist state.waiting_rooms |> List.map ~f:snd

let get_waiting_room state room_id = Map.find state.waiting_rooms room_id
let get_game_room state room_id = Map.find state.active_rooms room_id

let update_game_state state room_id new_game_state =
  match get_game_room state room_id with
  | None -> state
  | Some room ->
      let updated = { room with game_state = new_game_state } in
      {
        state with
        active_rooms = Map.set state.active_rooms ~key:room_id ~data:updated;
      }

let leave_waiting_room state room_id player_id =
  match get_waiting_room state room_id with
  | None -> state
  | Some room ->
      let players =
        List.filter room.players ~f:(fun p -> not (String.equal p.id player_id))
      in
      if List.is_empty players || String.equal room.host_id player_id then
        { state with waiting_rooms = Map.remove state.waiting_rooms room_id }
      else
        let updated_room = { room with players } in
        {
          state with
          waiting_rooms =
            Map.set state.waiting_rooms ~key:room_id ~data:updated_room;
        }

let find_room_by_player_id state player_id =
  let active =
    Map.to_alist state.active_rooms
    |> List.find_map ~f:(fun (id, room) ->
           if List.exists room.player_socket_ids ~f:(String.equal player_id)
           then Some (Game_room (id, room))
           else None)
  in
  match active with
  | Some room -> room
  | None -> (
      match
        Map.to_alist state.waiting_rooms
        |> List.find_map ~f:(fun (id, room) ->
               if
                 List.exists room.players ~f:(fun p ->
                     String.equal p.id player_id)
               then Some (Waiting_room (id, room))
               else None)
      with
      | Some room -> room
      | None -> No_room)

let replace_player_with_ai state room_id player_id difficulty =
  match get_game_room state room_id with
  | None -> (false, None, None, None, Some "Game room not found")
  | Some room -> (
      match
        find_index (fun id -> String.equal id player_id) room.player_socket_ids
      with
      | None -> (false, None, None, None, Some "Player not found in room")
      | Some idx ->
          let ai_id =
            Printf.sprintf "ai-%f-%d"
              (Core_unix.gettimeofday ())
              (Random.bits ())
          in
          let ai_name =
            "AI Bot ("
            ^ (match difficulty with
              | Ai.Easy -> "Easy"
              | Ai.Medium -> "Medium"
              | Ai.Hard -> "Hard")
            ^ ")"
          in
          let socket_ids =
            List.mapi room.player_socket_ids ~f:(fun i id ->
                if i = idx then ai_id else id)
          in
          let players =
            List.mapi room.game_state.players ~f:(fun i p ->
                if i = idx then Player.set_name p ai_name else p)
          in
          let game_state = { room.game_state with players } in
          let updated_room =
            { room with player_socket_ids = socket_ids; game_state }
          in
          let active_rooms =
            Map.set state.active_rooms ~key:room_id ~data:updated_room
          in
          let state = { state with active_rooms } in
          let all_ai =
            List.for_all socket_ids ~f:(fun id ->
                String.length id >= 3 && String.(String.slice id 0 3 = "ai-"))
          in
          (true, Some state, Some updated_room, Some all_ai, None))

let remove_game_room state room_id =
  { state with active_rooms = Map.remove state.active_rooms room_id }