Creating a dice-roller in Elixir. D-10 and D-100 giving me fits.

I’m very new to programming, and elixir is my first language. If my verbage or vocabulary are slightly off, I appreciate your understanding.

The Scope:

I’m working on an RNG modeled on tabletop dice. A d2, d4, d6, d8, d10, d12, d20 and d100. These will be called by functions needing to draw from charts, and will hopefully be rolled direclty by users as needed. As such, I need the results to be consistant and usable.

The Problem:

Anything over d8 is giving me fits in its result. It produces random seeming letters or punctuation instead of a numerical result.

I understand this is an artifact of how elixir and erlang sort of… co-mingle numbers and letters. The details and specifics of that are still a bit beyond me, I confess. I solved this issue in one specific instance, by piping the result of a single d100 into an Enum.sum. This doesn’t work for multiple dice, and won’t work globally.

The Code
Dice Module:

defmodule Plotgenerator.Dice do

@moduledoc """

Dice Functions

The basic dice functions are very simple, but effective.


A function call for 'Dice.dX(Y)', where X is the sides on the die and Y is the number being rolled, will return a list of results. This works by feeding the sides and count argument to Dice.roll, which in turn, calls on the Dice.d(sides) function.

The Dice.dX functions can be bypassed by using roll directly and providing it with the sides and count arguments.

Additionally, a functioncall of Dice.dX() with no arguments, will cause a single result of the die-type to be generated and returned.
"""

# NOTE this format replicates the process for d2(count) and provides a count of 1 for a d2 function with no argument.
  def d2(), do: d2(1)


  # NOTE this d2 command is the standard, providing a
  def d2(count) do
    roll(2, count)
  end


 # NOTE this format replicates the process for d2(), but provides a default value for the count. This is more complicated than the previous two iterations. We are not using it, but I am providing it for completeness.
  #
  #  def d2(count \\ 1) do
  #   roll(2, count)
  # end

# NOTE FOR ELIXIR FORUM - I have cut the other dice rollers from this, because they are exact formulaeic copies of the above examples, save for different numerals. 
  
# NOTE the defp deffinition means its private and can only be called by this module. This keeps other modules from accidentally calling it. Only use it if there is no other case in which another module may -need- to call it.
  defp d(sides) do
   Enum.random(1..sides)
  end

  @spec roll(pos_integer, pos_integer) :: list
  def roll(sides, count) when is_integer(count) and count > 0 and is_integer(sides) and sides > 1 do
    (1..count |> Enum.map(fn _-> d(sides) end))
  end

Thats the die rolling module.

Here is where I am trying to use it, and initially ran into the problem.

    def create(attributes \\ %{}) do
      defaults = %{
        shape: Plotgenerator.pick("traps/effect_shape"),
        size: area_select(),
        damage: Plotgenerator.pick("traps/damage_type"),
        difficulty: Enum.random(1..10),
        effect: Plotgenerator.pick("traps/trap_effect"),
        trigger_point: Plotgenerator.pick("traps/trap_locations"),
        control_point: Plotgenerator.pick("traps/trap_locations"),
        focal_point: Plotgenerator.pick("traps/trap_locations"),
        mechanism_point: Plotgenerator.pick("traps/trap_locations"),
        object: Plotgenerator.pick("traps/trigger_objects"),
        reset: Plotgenerator.pick("traps/reset_type"),
        time: Plotgenerator.pick("traps/reset_time"),
        triggered_by: Plotgenerator.pick("traps/trigger_action")
      }
      struct(__MODULE__, Map.merge(defaults, attributes))
    end

    # NOTE: when returning this value initially, it was printing as random characters. Thats because a list of numbers have corrolation to charlist, and it was offering that. By adding enum.sum to the end of it, we ensure it returns a number.

    def area_select() do
      Plotgenerator.Dice.d100(1) |> Enum.sum
    end

I solved the issue with the enum.sum on area_select(), but this is not a workable solution on the whole. Below is an example of the return, when I do not engage the enum.sum function on the result.

Note: I know the text doesn’t make a lot of sense. This is still getting the information to populate properly. I’ll clean up the template later.

If triggered, this trap has the effect of Illusion. If triggered, this trap effects a Geometric shaped area, with a size of E. This area is centered on Floor. The lethality of the trap is 8 out of 10. 

If triggered, this trap has the effect of Damage. If triggered, this trap effects a Cone shaped area, with a size of ;. This area is centered on Window, drain or vent. The lethality of the trap is 3 out of 10. 

If triggered, this trap has the effect of Alarm. If triggered, this trap effects a Wall shaped area, with a size of %. This area is centered on Stairs. The lethality of the trap is 6 out of 10. 

If triggered, this trap has the effect of Spell. If triggered, this trap effects a Wall shaped area, with a size of +. This area is centered on Stairs. The lethality of the trap is 6 out of 10. 

If triggered, this trap has the effect of Illusion. If triggered, this trap effects a Wall shaped area, with a size of M. This area is centered on Stairs. The lethality of the trap is 6 out of 10. 

Thank you for any suggestions, solutions or feedback you can offer.

I didn’t see the code for this template in the post, but a common cause of “my array of small-ish integers turned into random characters” is printing that array in a way that Elixir interprets as printing an Erlang-style charlist (an array of integers between 33 and 255).

For instance, IO.puts [69] will print the character E. Interpolation with #{ } will also do this:

s = [69]
IO.puts "interp: #{s}"

prints interp: E

Hey, thanks for the response. I did’t include the template, you’re right. I will add it below. Its a .eex file. Again, the language is not smooth, I’ve got plans to build different templates with conditional switching based on what the create function draws from the list of attributes, but for now, this is just testing that things come up properly in the first place.

trap.eex

This trap is triggered by <%= subject.triggered_by %> near a <%= subject.trigger_point %>. 

If triggered, this trap has the effect of <%= subject.effect %>. If triggered, this trap effects a <%= subject.shape %> shaped area, with a size of <%= subject.size %>. This area is centered on <%= subject.focal_point %>. The lethality of the trap is <%= subject.difficulty %> out of 10. 

The trap's controls are located near a <%= subject.control_point %>, it's mechanical componants are located near a <%= subject.mechanism_point %>. The trap has the following reset protocol: <%= subject.reset %>. 

 <%= subject.object %>

As for the rest - yeah, thats what I am seeing. I need a way to make it report the integer, not the character it could also be.

I literally want a number, but thats harder than it looks, for me.

In my mental model, there are two use-cases for functions like the Dice.dNNN:

  • the list of the individual die rolls is important. Useful for operations like “roll 4d6 and drop the lowest”

  • just the total of the dice is important. Useful for operations like “size is 3d12 feet”

If you just want the value of a single die roll, the second form is really what you want. I recommend keeping the existing d functions and adding total_dNNN (or a better name) functions.

I want both cases, really.

1: A listing of the individual numbers of the dice. I need to figure out a way to report the values of the randoms into integers that don’t shift to characters… which is my core issue. I am using IO.puts right now, to return values given by my eex evaluation function. Hrm.

2: The option to sum them up. Which I can do with Enum.sum. It sounds like I need a function that can call Dice.dN and Dice.roll() and then report the sum. That sounds like a good idea, actually.

You’ve gotten my mind moving down new paths. Thank you!

They don’t “shift to” characters: IO.puts is not the correct function to print lists of integers. It expects its argument to take one of two forms

  • a binary (Elixir’s string type), represented literally as a double-quoted string "foo"
  • a charlist (Erlang’s original string type), which is a list of integers each between 0 and 0x10FFFF

When you pass IO.puts a list of die rolls, it interprets it as the second form.

The same thing happens with string interpolation - "some string with #{a_list_of_integers}" will interpret a_list_of_integers as a charlist.

You may eventually want to define your own function for formatting lists of integers, but a cheap and straightforward way to get closer to what you want is Kernel.inspect. "some string with #{inspect a_list_of_integers}" will produce a list with square brackets and commas etc

Sorry, I haven’t read the whole discussion but still thought I’d chime in …

Inspecting lists of integers might still result in unexpected output:

iex(1)> "some string with #{inspect([50, 51])}"
"some string with '23'"

# can be "fixed" with `[charlists: :as:lists]` option, for more see https://hexdocs.pm/elixir/1.12/Inspect.Opts.html
iex(2)> "some string with #{inspect([50, 51], charlists: :as_lists)}"
"some string with [50, 51]"
1 Like

Maybe I’m missing something, but won’t Integer.to_string do the trick here?

https://hexdocs.pm/elixir/1.12/Integer.html#to_string/2

Sure, but it will raise an error on an invalid input which might not be desired here.