defmodule ServerMacros do @moduledoc """ This module defines some helper macros that are used within the server macro """ @doc """ This macro creates a wait loop to wait for the messages that are receive match what is inside the loop """ defmacro create_loop(name, do: match_exp) do function_name = :"#{name}_loop" ast1 = quote do {:timeout} -> :timeout end ast2 = quote do value -> # TODO check spelling log("Disreguarding: #{inspect(value)}") # Process.send_after(self(), value, t + 2000) unquote(function_name)(v, t) end ast3 = ast1 ++ match_exp ++ ast2 quote do defp unquote(function_name)(v, t) do var!(v) = v receive do unquote(ast3) after t -> :timeout end end end end @doc """ this function tries to propose and on failure it calls the else block and on success tries to match with the expressions on the do block """ defmacro try_propose(val, do: ready, else: recal_do) do ast1 = quote do {:timeout} -> unquote(recal_do) {:abort} -> unquote(recal_do) end ast2 = quote do {:decision, v} -> var!(state) = apply_game(var!(state), v) unquote(recal_do) v -> raise "Unknown message on try_propose #{inspect(v)}" end ast3 = ast1 ++ ready ++ ast2 quote do v = Paxos.propose(var!(state).paxos, var!(state).instance, unquote(val), 1000) case v do unquote(ast3) end end end end defmodule Server do @moduledoc """ Contains the to run the server Code """ require ServerMacros import ServerMacros require Utils import Utils create_log 2 @doc """ Contains the start code for the server """ def start(name, participants) do log("#{name} Starting server") pid = spawn(Server, :init, [name, participants]) register_name(name, pid, false) end @doc """ Initializes the state and starts the paxos inspect and then it calls the run fn """ def init(name, participants) do paxos = Paxos.start(alter_name(name, "_paxos"), Enum.map(participants, fn name -> alter_name(name, "_paxos") end), true) state = %{ name: name, procs: participants, games: %{}, paxos: paxos, instance: 0, } run(state) end runfn do {:start_game, participants, pid_to_inform} -> or_state is_list(participants) do {state, game_id} = try_to_create_game(state, participants) state = set_modifed(state, game_id) safecast(pid_to_inform, {:start_game_ans, game_id}) state end {:get_game_state, game_id, pid_to_inform} -> state = get_game_state(state, game_id, pid_to_inform) set_modifed(state, game_id) {:make_move, game_id, move, pid_to_inform} -> try_to_play_checks(state, game_id, move, pid_to_inform) end # Checks if the user can play the move before starting the requesting the user to play defp try_to_play_checks(state, game_id, move, pid_to_inform, repeat \\ false) do cond do state.games[game_id] == :not_playing_in_game -> safecast(pid_to_inform, {:make_move, game_id, :not_playing}) state state.games[game_id] == nil -> if repeat do safecast(pid_to_inform, {:make_move, game_id, :game_does_not_exist}) state else state = qurey_status(state) try_to_play_checks(state, game_id, move, pid_to_inform, true) end true -> state = qurey_status(state) game = state.games[game_id] cond do is_finished(state, game_id) -> {_, score} = state.games[game_id] safecast(pid_to_inform, {:make_move, game_id, :game_finished, score}) state game.modified -> safecast(pid_to_inform, {:make_move, game_id, :player_moved_before, game.game_state, game.hand}) set_modifed(state, game_id) true -> cond do not is_number(move) -> safecast(pid_to_inform, {:make_move, game_id, :invalid_move}) state move >= length(game.game_state) or move < 0 -> safecast(pid_to_inform, {:make_move, game_id, :invalid_move}) state true -> try_to_play(state, game_id, move, pid_to_inform) end end end end # Tries to propose to paxos the game action defp try_to_play(state, game_id, move, pid_to_inform) do name = state.name new_hand = if state.games[game_id].hand[state.name] == :- do Enum.at(state.games[game_id].game_state, move) else get_hand_for_game_state(state.games[game_id].game_state) end try_propose {:make_move, game_id, name, move, new_hand} do {:decision, {:make_move, ^game_id, ^name, ^move, ^new_hand}} -> state = apply_game(state, {:make_move, game_id, name, move, new_hand}) if is_finished(state, game_id) do {_, score} = state.games[game_id] safecast(pid_to_inform, {:make_move, game_id, :game_finished, score}) else game = state.games[game_id] safecast(pid_to_inform, {:make_move, game_id, game.game_state, game.hand[state.name]}) end set_modifed(state, game_id) {:decision, {:make_move, ^game_id, new_name, new_move, new_new_hand}} -> state = apply_game(state, {:make_move, game_id, new_name, new_move, new_new_hand}) if is_finished(state, game_id) do {_, score} = state.games[game_id] safecast(pid_to_inform, {:make_move, game_id, :game_finished, score}) else game = state.games[game_id] safecast(pid_to_inform, {:make_move, game_id, :player_moved_before, game.game_state, game.hand}) end set_modifed(state, game_id) else try_to_play(state, game_id, move, pid_to_inform) end end # Get the most recent game_state and return it to the player defp get_game_state(state, game_id, pid_to_inform, repeat \\ false) do cond do state.games[game_id] == :not_playing_in_game -> safecast(pid_to_inform, {:game_state, game_id, :not_playing}) state state.games[game_id] == nil -> if repeat do safecast(pid_to_inform, {:game_state, game_id, :game_does_not_exist}) state else state = qurey_status(state) get_game_state(state, game_id, pid_to_inform, true) end true -> state = qurey_status(state) if is_finished(state, game_id) do {_, score} = state.games[game_id] safecast(pid_to_inform, {:game_state, game_id, :game_finished, score}) else game = state.games[game_id] safecast(pid_to_inform, {:game_state, game_id, game.game_state, game.hand[state.name]}) end state end end # This generates a new hand based on the current game state defp get_hand_for_game_state(game_state) do r1 = Enum.random(0..100) cond do r1 <= 1 -> :b r1 <= 5 -> :- r1 <= 20 -> :+ true -> mx = game_state |> Enum.filter(fn m -> m != :+ end) |> Enum.max() mn = max(mx - 20, 1) mx = max(mx - 2, 4) Enum.random(mn..mx) end end # This tries to create a game by sending the create message to paxos defp try_to_create_game(state, participants) do game_ids = Map.keys(state.games) latest = Enum.at(Enum.sort(game_ids), length(game_ids) - 1) new_game_id = if latest do latest else 0 end + 1 new_game_state = Enum.to_list(0..Enum.random(3..8)) |> Enum.map(fn _ -> Enum.random(1..4) end) hand = Enum.reduce(participants, %{}, fn p, acc -> Map.put(acc, p, get_hand_for_game_state(new_game_state)) end) try_propose {:start_game, new_game_id, participants, new_game_state, hand} do {:decision, {:start_game, ^new_game_id, ^participants, ^new_game_state, ^hand}} -> state = apply_game(state, {:start_game, new_game_id, participants, new_game_state, hand}) {state, new_game_id} else try_to_create_game(state, participants) end end # # Utils # # Checks if a game has been finished defp is_finished(state, game) do case state.games[game] do {:finished, _} -> true _ -> false end end # Gets up to the most recent instance defp qurey_status(state) do v = Paxos.get_decision(state.paxos, state.instance, 100) or_state v != nil do state = apply_game(state, v) qurey_status(state) end end # Sets the modified flag defp set_modifed(state, game, val \\ false) do or_state not is_finished(state, game) and state.games[game] != :not_playing_in_game and state.games[game] != nil do %{state | games: Map.put(state.games, game, %{state.games[game] | modified: val})} end end # # Apply Game States # # Calculates the index based on the incoming index and a move defp get_index(indexed_game_state, spos, index) do index = spos + index len = length(indexed_game_state) cond do index < 0 -> len - 1 index >= len -> rem(index, len) true -> index end end # This funcion looks at pluses and blackholes and chekes if they can be merged # This funcion will pick a state like this [ 1, 2, :+, 2, 1 ] and create a new state [ 1, {:merged, 3} , 1] and then call expand merge which will expand the merge # Note by this point the state is indexed so it looks like [{1, 0}, {:+, 1}, {1, 2}] defp simplify_game_state_pluses([], indexed_game_state), do: {false, indexed_game_state} defp simplify_game_state_pluses([{:+, i} | tl], indexed_game_state) do before_i = get_index(indexed_game_state, i, -1) after_i = get_index(indexed_game_state, i, 1) if before_i != after_i do {b, b_i} = Enum.at(indexed_game_state, before_i) {a, a_i} = Enum.at(indexed_game_state, after_i) if b == a do case b do :+ -> simplify_game_state_pluses(tl, indexed_game_state) :b -> simplify_game_state_pluses(tl, indexed_game_state) n -> list = indexed_game_state |> Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, n + 1}, i}, else: {x, ti} end) |> Enum.filter(fn {_, ti} -> cond do b_i == ti -> false a_i == ti -> false true -> true end end) |> reindex() {true, expand_merge(list)} end else simplify_game_state_pluses(tl, indexed_game_state) end else simplify_game_state_pluses(tl, indexed_game_state) end end defp simplify_game_state_pluses([{:b, i} | tl], indexed_game_state) do before_i = get_index(indexed_game_state, i, -1) after_i = get_index(indexed_game_state, i, 1) if before_i != after_i do {b, b_i} = Enum.at(indexed_game_state, before_i) {a, a_i} = Enum.at(indexed_game_state, after_i) a = if is_atom(a) do 1 else a end b = if is_atom(b) do 1 else b end list = indexed_game_state |> Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, trunc(:math.floor((a + b) / 2))}, i}, else: {x, ti} end) |> Enum.filter(fn {_, ti} -> cond do b_i == ti -> false a_i == ti -> false true -> true end end) |> reindex() {true, expand_merge(list)} else simplify_game_state_pluses(tl, indexed_game_state) end end # Check if the item in an game is the substate merged defp is_merged(item) do case item do {:merged, _} -> true _ -> false end end # This funcion expands merges # With a state like this [1, {:merged, 3}, 1] it will create a game state like this [ {:merged, 4 } ] # Note by this point the state is indexed so it looks like [{1, 0}, {:+, 1}, {1, 2}] defp expand_merge(indexed_game_state) do {{:merged, n}, i} = indexed_game_state |> Enum.find(fn {x, _} -> is_merged(x) end) b_i = get_index(indexed_game_state, i, - 1) a_i = get_index(indexed_game_state, i, + 1) if b_i != a_i do {b, b_i} = Enum.at(indexed_game_state, b_i) {a, a_i} = Enum.at(indexed_game_state, a_i) if a == b do case b do :+ -> indexed_game_state {:merged, _} -> indexed_game_state _ -> indexed_game_state |> Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, max(n, a) + 1}, i}, else: {x, ti} end) |> Enum.filter(fn {_, ti} -> cond do b_i == ti -> false a_i == ti -> false true -> true end end) |> reindex() |> expand_merge() end else indexed_game_state end else indexed_game_state end end # This funcion removes previous indexes and reindexs the state defp reindex(list, flat \\ true) do list = if flat do list |> Enum.map(fn {n, _} -> n end) else list end [list, 0..(length(list) - 1)] |> Enum.zip() end # Removes all merged from the array defp remove_merged(l), do: l |> Enum.map(fn x -> case x do {:merged, n} -> n x -> x end end) # This funcion recieves the game state after the move was done and tries to simplify the game defp simplify_game_state(game_state) do indexed_game_state = game_state |> reindex(false) {repeat, indexed_game_state} = indexed_game_state |> Enum.filter(fn x -> case x do {:+, _} -> true {:b, _} -> true _ -> false end end) |> simplify_game_state_pluses(indexed_game_state) if repeat do indexed_game_state |> Enum.map(fn {v, _} -> v end) |> remove_merged() |> simplify_game_state() else indexed_game_state |> Enum.map(fn {v, _} -> v end) end end # This function tries to apply the make_move command defp apply_game(state, {:make_move, game_id, player_name, pos_move, new_hand}) do game = state.games[game_id] case game do {:finished, _} -> raise "Game already finished" :not_playing_in_game -> %{state | instance: state.instance + 1 } game -> game_state = game.game_state game_state = if game.hand[player_name] == :- do List.delete_at(game_state, pos_move) else {b, e} = Enum.split(game_state, pos_move) b ++ [ game.hand[player_name] ] ++ e end game_state = simplify_game_state(game_state) hand = Map.put(game.hand, player_name, new_hand) game = %{game| hand: hand, game_state: game_state, modified: true } if length(game.game_state) > 16 do %{state| games: Map.put(state.games, game_id, { :finished, game.game_state |> Enum.filter(fn x -> not is_atom(x) end) |> Enum.sum()}), instance: state.instance + 1 } else %{state| games: Map.put(state.games, game_id, game), instance: state.instance + 1} end end end # This function tries to apply the start_game command defp apply_game(state, {:start_game, game_id, participants, new_game_state, hand}) do cond do state.games[game_id] -> raise "Game Already Exists" state.name in participants -> %{state | games: Map.put(state.games, game_id, %{ game_state: new_game_state, participants: participants, hand: hand, modified: true, }), instance: state.instance + 1 } true -> %{state | games: Map.put(state.games, game_id, :not_playing_in_game), instance: state.instance + 1, } end end defp apply_game(_, _), do: raise "Do not know how to apply game state" ############ # Interface ############ # handles responses from the start game request create_loop :start_game do {:start_game_ans, game_id} -> log("Started a game #{game_id}") {:start_game, game_id} end @doc """ Requests the server to start a game """ def start_game(name, participants) do safecast(name, {:start_game, participants, self()}) start_game_loop(nil, 10000) end # handles responses from the get game state request create_loop :get_game_state do {:game_state, ^v, :not_playing} -> log("Not Playing in that game") {:not_playing} {:game_state, ^v, :game_finished, score} -> log("Game finsihed, #{score}") {:game_finished, score} {:game_state, ^v, game_state, hand} -> log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}") {:state, game_state, hand} {:game_state, ^v, :game_does_not_exist} -> log("Got game does not exist") {:not_exists} end @doc """ Requests the server to get the game state """ def get_game_state(name, game_id) do safecast(name, {:get_game_state, game_id, self()}) get_game_state_loop(game_id, 10000) end # handles responses from the make move request create_loop :make_move do {:make_move, ^v, :game_does_not_exist} -> log("Got game does not exist") {:not_exists} {:make_move, ^v, :not_playing} -> log("Not Playing in that game") {:not_playing} {:make_move, ^v, :game_finished, score} -> log("Game finsihed, #{score}") {:game_finished, score} {:make_move, ^v, :player_moved_before, game_state, hand} -> log("Player moved_before, #{inspect(game_state)} #{inspect(hand)}") {:player_moved_before, game_state, hand} {:make_move, ^v, :invalid_move} -> log("Invalid Move") {:invalid_move} {:make_move, ^v, game_state, hand} -> log("Got game state, #{inspect(game_state)}, hand: #{inspect(hand)}") {:state, game_state, hand} end @doc """ Requests to make a move """ def make_move(name, game_id, move) do safecast(name, {:make_move, game_id, move, self()}) make_move_loop(game_id, 10000) end ############ # Debug ############ @doc """ Quicky creates some servers it's useful in testing """ def spinup(number_of_participants) do procs = Enum.to_list(1..number_of_participants) |> Enum.map(fn n -> :"p#{n}" end) Enum.map(procs, fn proc -> Server.start(proc, procs) end) end @doc """ Quicky kills some servers it's useful in testing """ def kill (pids) do pids |> Enum.map(fn m -> Process.exit(m, :kill) end) end end defmodule Client do @moduledoc """ This module handles displaying the game state in a nice to understand way """ import Utils require Utils create_log 3 @doc """ This function plays the displays the game then waits for user input and displays the next state of the game """ def play_game(process, game_id) do game = Server.get_game_state(process, game_id) cont = case game do :timeout -> log("Could to not comunicate with the server") true {:not_exists} -> log("Game does not exist") true {:not_playing} -> log("Not Playing in that game") true {:game_finished, score} -> log("Game finsihed, #{score}") true {:game_does_not_exist} -> log("Got game does not exist") true {:state, game_state, hand} -> game_state |> to_name() |> interpolate() |> fill() |> printpt(to_name(hand)) false end if not cont do to_play = IO.gets("Type the number you want to play or q to exit: ") to_play = to_play |> String.trim("\"") |> String.trim() case to_play do "q" -> log("Bye Bye") v -> try do {n, _} = Integer.parse(v) res = Server.make_move(process, game_id, n) case res do :timeout -> log("Could to not comunicate with the server") {:not_exists} -> log("Game does not exist") {:not_playing} -> log("Not Playing in that game") {:game_finished, score} -> log("Game finsihed, #{score}") {:game_does_not_exist} -> log("Got game does not exist") {:player_moved_before, _, _} -> log("Player Moved before you did please check the map and try again") play_game(process, game_id) {:invalid_move} -> raise "Invalid Move" {:state, _, _} -> play_game(process, game_id) end rescue _ -> log("Please provide a valid number") play_game(process, game_id) end end end end @doc """ This funcion waits for user input and then commands the server to act Use the display_game funcion in a seperate terminal to see the game """ def control_game(process, game_id) do to_play = IO.gets("Type the number you want to play or q to exit: ") to_play = to_play |> String.trim("\"") |> String.trim() case to_play do "q" -> log("Bye Bye") v -> try do {n, _} = Integer.parse(v) res = Server.make_move(process, game_id, n) case res do :timeout -> log("Could to not comunicate with the server") {:not_exists} -> control_game(process, game_id) {:not_playing} -> control_game(process, game_id) {:game_finished, _} -> control_game(process, game_id) {:player_moved_before, _, _} -> log("Player Moved before you did please check the map and try again") control_game(process, game_id) {:invalid_move} -> raise "Invalid Move" {:state, _, _} -> control_game(process, game_id) end rescue _ -> log("Please provide a valid number") control_game(process, game_id) end end end @doc """ This funcion tries to display the most recent version of the game Use control_game to control game """ def display_game(process, game_id, temp_game \\ [], temp_hand \\ []) do game = Server.get_game_state(process, game_id) case game do :timeout -> log("Could to not comunicate with the server") {:not_exists} -> log("Game does not exist") {:not_playing} -> log("Not Playing in that game") {:game_finished, score} -> log("Game finsihed, #{score}") {:game_does_not_exist} -> log("Got game does not exist") {:state, game_state, hand} -> if temp_game != game_state or temp_hand != hand do # IO.ANSI.clear() game_state |> to_name() |> interpolate() |> fill() |> printpt(to_name(hand)) Process.sleep(1000) display_game(process, game_id, game_state, hand) else Process.sleep(1000) display_game(process, game_id, temp_game, temp_hand) end end end # This funcion recieves some objects and transforms them into user friendly text defp to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end) defp to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> IO.ANSI.color(15) <> " + " <> IO.ANSI.reset() defp to_name(atom) when atom == :-, do: IO.ANSI.color_background(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset() defp to_name(atom) when atom == :b, do: IO.ANSI.color_background(232) <> IO.ANSI.color(15) <> " b " <> IO.ANSI.reset() defp to_name(atom) when is_atom(atom), do: atom defp to_name(i) do letter = [ "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og" ] |> Enum.at(i - 1) color = [46,45,138,19,11,140,8,47,57,159,72,48,55,35,251,188,107,110,21,11,156,134,128,179,140,234,14,90,206,7,249,209,253,123,192,165,234,136,198,208,43,34,215,127,23,250,177,237,124,202,229, 63,206,220,224,109,202,113,253,7,243,26,160,65,39,112,57,75, 252,82,213,186,68,243,134,100,226,48,90,134,208,102,25,106,72, 242,26,59,166,26,187,54,194,165,97,219,186,130,7,154,233,85, 130,67,43,200,90,60,148,49,161,110,247,116,223,159,132,132] |> Enum.at(i - 1) |> IO.ANSI.color() color <> String.pad_leading("#{letter}", 3, " ") <> IO.ANSI.reset() end # Adds the index as an indicator and pads the index defp interpolate(list) do [list, 0..length(list) - 1] |> Enum.zip() |> Enum.reduce([], fn {v, i}, acc -> acc ++ [String.pad_leading("#{i}", 3, " "), v] end) end # This grows a list with between spaces every item of the interpolated list # This allows the items to take as puch space in the possilble defp grow_empty_list(t, i, acc) when i == 0, do: t ++ acc defp grow_empty_list([], i, acc), do: grow_empty_list(acc, i, []) defp grow_empty_list([h | tl], i, acc), do: grow_empty_list(tl, i - 1, acc ++ [ h ++ [" "] ]) # This takes the list that was generated in the grow_empty_list function and merges it between every other item defp fill(list) do to_fill = 32 - length(list) to_add = grow_empty_list(Enum.map(list, fn _ -> [] end), to_fill, []) fill(list, to_add, []) end defp fill([], _, acc), do: acc defp fill([hd | tail], [add_hd | add_tail], acc) do fill(tail, add_tail ,acc ++ [hd] ++ add_hd) end # This functions prints the circle defp printpt(game_state, hand), do: printpt(game_state, hand, 0) defp printpt(_, _, i) when i > 16, do: nil defp printpt(game_state, hand, i) do res = case i do 0 -> " xxx \n" |> String.replace("xxx", Enum.at(game_state, 1)) 1 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 0)) |> String.replace("yyy", Enum.at(game_state, 2)) 2 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 31)) |> String.replace("yyy", Enum.at(game_state, 3)) 3 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 30)) |> String.replace("yyy", Enum.at(game_state, 4)) 4 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 29)) |> String.replace("yyy", Enum.at(game_state, 5)) 5 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 28)) |> String.replace("yyy", Enum.at(game_state, 6)) 6 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 27)) |> String.replace("yyy", Enum.at(game_state, 7)) 7 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 26)) |> String.replace("yyy", Enum.at(game_state, 8)) 8 -> " xxx zzz yyy \n" |> String.replace("xxx", Enum.at(game_state, 25)) |> String.replace("yyy", Enum.at(game_state, 9)) |> String.replace("zzz", hand) 9 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 24)) |> String.replace("yyy", Enum.at(game_state, 10)) 10 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 23)) |> String.replace("yyy", Enum.at(game_state, 11)) 11 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 22)) |> String.replace("yyy", Enum.at(game_state, 12)) 12 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 21)) |> String.replace("yyy", Enum.at(game_state, 13)) 13 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 20)) |> String.replace("yyy", Enum.at(game_state, 14)) 14 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 19)) |> String.replace("yyy", Enum.at(game_state, 15)) 15 -> " xxx yyy \n" |> String.replace("xxx", Enum.at(game_state, 18)) |> String.replace("yyy", Enum.at(game_state, 16)) 16 -> " xxx \n" |> String.replace("xxx", Enum.at(game_state, 17)) end IO.write("#{res}") printpt(game_state, hand, i + 1) end end