# Dice of Doom Game in Elixir, Lazy Version with Human Player Only # CSci 556: Multiparadigm Programming, Spring 2015 # H. Conrad Cunningham, Professor # Computer and Information Science # University of Mississippi #234567890123456789012345678901234567890123456789012345678901234567890 # 2015-02-27: Began development following Land of Lisp # 2015-03-01: Prototyped eager version with human player only # 2015-03-07: Prototyped lazy version with human player only from # eager version # 2015-03-11: Cleaned the code in various ways and added comments defmodule Dice2 do # Set game size constants @num_players 2 @max_dice 3 @board_size 4 defp board_hexnum do @board_size * @board_size end # Create an empty new board, which must be a Dict. def new_board() do %{} end # Generate a random board. This directly uses Erlang :random module. def gen_board() do Enum.into(0..board_hexnum-1, new_board(), fn n -> { n, { :random.uniform(@num_players)-1, :random.uniform(@max_dice) } } end) end # Map player integer identifier to matching lowercase letter. def player_letter(n) do String.at("abcdefghijklmnopqrstuvwxyz",n) end # Print the game board as text. def draw_board(board) do IO.puts board_as_string(board) end # board_as_string was factored out of draw_board. def board_as_string(board) do Enum.map_join(0..@board_size-1, "\n", fn y -> String.duplicate(" ",@board_size-y) <> (Enum.map_join(0..@board_size-1, fn x -> {p,d} = board[x + (@board_size * y)] "#{player_letter(p)}-#{d} " end)) end) end # Generate a game tree for the current configuration of board, # current player, and number of spare dice. Boolean argument # first_move indicates whether this is the player's first move (in # which passing is not allowed)> def game_tree(board, player, spare_dice, first_move) do { player, board, add_passing_move(board, player, spare_dice, first_move, attacking_moves(board, player, spare_dice)) } end # Add a passing move (nil) to the list of possible moves if allowed. def add_passing_move(_,_,_,true,moves) do moves # first_move argument true, cannot pass end # Lazy mod: Use Stream.concat instead of eager "cons". def add_passing_move(board,player,spare_dice,_,moves) do # first_move argument false, can pass Stream.concat( [{ nil, game_tree(add_new_dice(board,player,spare_dice-1), rem(player+1,@num_players), 0, true ) }], moves) end # Add all attaching moves for the current board, player, and spare # dice configuration. # Lazy mod: Use Stream.filter_map and Stream.concat instead of the # similar Enum functions. def attacking_moves(board, cur_player, spare_dice) do Stream.filter_map(board, fn {_,{p,_}} -> p == cur_player end, fn {src,{p0,d0}} -> Enum.map(neighbors(src),&({&1,board[&1]})) |> Enum.filter_map( fn {_,{p1,d1}} -> p0 != p1 and d0 > d1 end, fn {dst,{_,d1}}-> { {src,dst}, game_tree( board_attack(board,cur_player,src,dst,d0), cur_player, spare_dice + d1, false ) } end ) end) |> Stream.concat() end # Return a list of all neighbors of hexagon src in the board. def neighbors(pos) do up = pos - @board_size down = pos + @board_size up_left = if pos >= @board_size and rem(pos,@board_size) != 0 do [up-1] else [] end up_right = if pos >= @board_size do [up] else [] end left = if rem(pos,@board_size) != 0 do [pos-1] else [] end right = if rem(pos+1,@board_size) != 0 do [pos+1] else [] end down_left = if pos < (board_hexnum - @board_size) do [down] else [] end down_right = if pos < (board_hexnum - @board_size) and rem(pos+1,@board_size) != 0 do [down+1] else [] end up_left ++ up_right ++ left ++ right ++ down_left ++ down_right end # Update game board based on results of attack from src to dst. def board_attack(board, _, src, dst, dice) do {p0,_} = board[src] board |> Dict.put(src,{p0,1}) |> Dict.put(dst,{p0,dice-1}) end # Redistribute the dice to the capturing player. def add_new_dice(board,player,spare_dice) do changes = Enum.filter_map(board, fn {_,{p,d}} -> p == player and d < @max_dice end, fn {i,{p,d}} -> {i,{p,d+1}} end) |> Enum.take(spare_dice) |> Enum.into(new_board()) Dict.merge(board,changes) end # The code below is for human players, # Main game loop for only human players. # Lazy mod: Expand moves stream to list. def play_vs_human(tree) do print_info(tree) {player,board,moves} = tree moves_list = Enum.to_list(moves) expanded = {player,board,moves_list} case moves_list do [] -> IO.puts announce_winner(board) _ -> play_vs_human(handle_human(expanded)) end end # Print current player and game board. def print_info(tree) do {player,board,_} = tree IO.puts "\nCurrent player = #{player_letter(player)}" draw_board(board) end # Display menu, get move selection from human player, and move to # the new configuration by returning the new game tree. def handle_human(tree) do {_,_,moves} = tree menu = Enum.zip(1..board_hexnum,moves) |> Enum.map_join("\n", fn {n, {{src,dst},_}}-> "#{n}: #{src} -> #{dst}" {n, {nil,_}} -> "#{n}: end turn" end) IO.puts menu sel = IO.gets("Choose your move: ") |> String.strip |> String.downcase |> String.split(" ", trim: true) |> List.first |> String.to_integer selmove = if sel < 0 or sel > length(moves) do IO.puts "Invalid choice #{sel}. Forcing choice 1." 0 else sel-1 end moves |> Enum.at(selmove) |> elem(1) end # Return a list of winners. def winners(board) do init = List.duplicate(0,@num_players) sums = Enum.reduce(board,init, fn {_,{p,_}}, acc -> List.update_at(acc,p,&(&1+1)) end) best = Enum.max(sums) Enum.zip(0..(@num_players-1),sums) |> Enum.filter_map(&(elem(&1,1) == best),&(elem(&1,0))) end # Return a string to announce the list of winners. def announce_winner(board) do w = winners(board) if length(w) > 1 do "The game is a tie among: " <> Enum.map_join(w,", ", &("#{player_letter(&1)}")) else "The winner is #{player_letter(List.first(w))}" end end # Textual user interface to overall game. There is no equivalent in # Land of Lisp code. def play_game() do board = gen_board tree = game_tree(board,0,0,true) ans = IO.gets("Play against computer? ('y' or 'n') ") |> String.strip |> String.downcase |> String.split(" ", trim: true) |> List.first case ans do "y" -> # play_vs_computer(tree) IO.puts "Play against computer not yet supported." "n" -> play_vs_human(tree) _ -> IO.puts "Invalid input '#{ans}'. Playing against humqn." play_vs_human(tree) end end end