Squib Thinks in Arrays

When prototyping card games, you usually want some things (e.g. icons, text) to remain the same across every card, but then other things need to change per card. Maybe you want the same background for every card, but a different title.

The vast majority of Squib’s DSL methods can accept two kinds of input: whatever it’s expecting, or an array of whatever it’s expecting. If it’s an array, then Squib expects each element of the array to correspond to a different card.

Think of this like “singleton expansion”, where Squib goes “Is this an array? No? Then just repeat it the same across every card”. Thus, these two DSL calls are logically equivalent:

Squib::Deck.new(cards: 2) do
  text str: 'Hello'
  text str: ['Hello', 'Hello'] # same effect
end

But then to use a different string on each card you can do:

Squib::Deck.new(cards: 2) do
  text str: ['Hello', 'World']
end

Note

Technically, Squib is only looking for something that responds to each (i.e. an Enumerable). So whatever you give it should just respond to each and it will work as expected.

What if you have an array that doesn’t match the size of the deck? No problem - Ruby won’t complain about indexing an array out of bounds - it simply returns nil. So these are equivalent:

Squib::Deck.new(cards: 3) do
  text str: ['Hello', 'Hello']
  text str: ['Hello', 'Hello', nil]  # same effect
end

In the case of the text method, giving it an str: nil will mean the method won’t do anything for that card and move on. Different methods react differently to getting nil as an option, however, so watch out for that.

Using range to specify cards

There’s another way to limit a DSL method to certain cards: the range parameter.

Most of Squib’s DSL methods allow a range to be specified. This can be an actual Ruby Range, or anything that implements each (i.e. an Enumerable) that corresponds to the index of each card.

Integers are also supported for changing one card only. Negatives work from the back of the deck.

Some quick examples:

text range: 0..2  # only cards 0, 1, and 2
text range: [0,2] # only cards 0 and 2
text range: 0     # only card 0

Behold! The Power of Ruby Arrays

One of the more distinctive benefits of Ruby is in its rich set of features for manipulating and accessing arrays. Between range and using arrays, you can specify subsets of cards quite succinctly. The following methods in Ruby are particularly helpful:

  • Array#each - do something on each element of the array (Ruby folks seldom use for-loops!)
  • Array#map - do something on each element of an array and put it into a new array
  • Array#select - select a subset of an array
  • Enumerable#each_with_index - do something to each element, also being aware of the index
  • Enumerable#inject - accumulate elements of an array in a custom way
  • Array#zip - combine two arrays, element by element

Examples

Here are a few recipes for using arrays and ranges in practice:

 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
require 'squib'

data = { 'name' => ['Thief', 'Grifter', 'Mastermind'],
        'type' => ['Thug', 'Thinker', 'Thinker'],
        'level' => [1, 2, 3] }

Squib::Deck.new(width: 825, height: 1125, cards: 3) do
  # Default range is :all
  background color: :white
  text str: data['name'], x: 250, y: 55, font: 'Arial 18'
  text str: data['level'], x: 65, y: 40, font: 'Arial 24'

  # Could be explicit about using :all, too
  text range: :all,
       str: data['type'], x: 40, y: 128, font: 'Arial 6',
       width: 100, align: :center

  # Ranges are inclusive, zero-based
  text range: 0..1, str: 'Thief and Grifter only!!', x: 25, y:200

  # Integers are also allowed
  text range: 0, str: 'Thief only!', x: 25, y: 250

  # Negatives go from the back of the deck
  text range: -1, str: 'Mastermind only!', x: 25, y: 250
  text range: -2..-1, str: 'Grifter and Mastermind only!', x: 25, y: 650

  # We can use Arrays too!
  text range: [0, 2], str: 'Thief and Mastermind only!!', x: 25, y:300

  # Just about everything in Squib can be given an array that
  # corresponds to the deck's cards. This allows for each card to be styled differently
  # This renders three cards, with three strings that had three different colors at three different locations.
  text str: %w(red green blue),
       color: [:red, :green, :blue],
       x: [40, 80, 120],
       y: [700, 750, 800]

  # Useful idiom: construct a hash from card names back to its index (ID),
  # then use a range. No need to memorize IDs, and you can add cards easily
  id = {} ; data['name'].each_with_index{ |name, i| id[name] = i}
  text range: id['Thief']..id['Grifter'],
       str: 'Thief through Grifter with id lookup!!',
       x:25, y: 400

  # Useful idiom: generate arrays from a column called 'type'
  type = {}; data['type'].each_with_index{ |t, i| (type[t] ||= []) << i}
  text range: type['Thinker'],
       str: 'Only for Thinkers!',
       x:25, y: 500

  # Useful idiom: draw a different number of images for different cards
  hearts = [nil, 1, 2] # i.e. card 0 has no hearts, card 2 has 2 hearts drawn
  1.upto(2).each do |n|
    range = hearts.each_index.select { |i| hearts[i] == n}
    n.times do |i|
      svg file: 'glass-heart.svg', range: range,
          x: 150, y: 55 + i * 42, width: 40, height: 40
    end
  end

  rect stroke_color: 'black' # just a border
  save_sheet prefix: 'ranges_', columns: 3
end

Contribute Recipes!

There are a lot more great recipes we could come up with. Feel free to contribute! You can add them here via pull request or via the wiki