Skip to content

Instantly share code, notes, and snippets.

@kassi
Last active April 12, 2021 20:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kassi/8bc1a3c9c10c54bae297b9078694b33c to your computer and use it in GitHub Desktop.
Save kassi/8bc1a3c9c10c54bae297b9078694b33c to your computer and use it in GitHub Desktop.
Quick & dirty deck builder for Tabletop Simulator. No command line help yet. Configure at the ed of the script!
#!/usr/bin/env ruby
# encoding: utf-8
# tested with Ruby 2.7.2
# Builds a deck of cards or similar objects using a list of input images and (optional) multiplicators.
# All cards need to have same size
class TableTopDeckBuilder
attr_accessor :name, :glob, :back_image, :hidden_card_image, :mults, :remove_intermediates
attr_accessor :cards, :card_width, :card_height
# Creates a deck builder.
# param name: Name of the deck, used as the resulting filename.
# param glob: Glob to read the images needed for this deck.
# param back_image: Name of the image for the back side
# param hidden_card_image: Name of the image to be used as the face in hidden hands. If not set, TTS needs to use the back image.
# param mults: List of integers defining multipliers of each image.
# The order of the images is expected as the sorted glob.
# If not given, the default is 1 for each image.
# TODO: Will be changed in the future to allow a hash to be given.
# param remove_intermediates: If set, all intermediate image files will be removed. Disable for debugging.
def initialize(name:, glob:, back_image:, hidden_card_image: nil, mults: nil, remove_intermediates: true)
@name = name
@glob = glob
@back_image = back_image
@hidden_card_image = hidden_card_image
@remove_intermediates = remove_intermediates
@mults = mults
end
def build
prepare_deck!
deck_size = hidden_card_image ? 69 : 70
deck_number = cards.length > deck_size ? 1 : nil
while cards.length > 0 do
deck = Deck.new(
name: deck_name(name, deck_number),
cards: cards.slice!(0, deck_size),
card_width: card_width,
card_height: card_height,
back_image: back_image,
hidden_card_image: hidden_card_image,
remove_intermediates: remove_intermediates
)
deck.build
deck_number += 1 if deck_number
end
end
private
def prepare_deck!
images = read_images
@mults = [1] * images.length unless mults
abort "Error: No images found" if images.length == 0
abort "Error: Number of multiplicators doesn't match number of images found with glob (#{mults.length} ≠ #{images.length})." if mults.length != images.length
@cards = []
images.each_with_index do |image, index|
@cards.concat([image] * mults[index])
end
end
def read_images
images = []
Dir.glob(glob).sort.each do |file|
check_image!(file)
images << file
end
check_image!(back_image)
check_image!(hidden_card_image) if hidden_card_image
images
end
def check_image!(file)
w, h = image_size(file)
if @card_width
abort "Error: Image sizes don't match with previous: #{file}" if w != @card_width || h != @card_height
else
@card_width = w
@card_height = h
end
end
def deck_name(name, number)
return name unless number
"#{name}_#{number}"
end
def image_size(file)
result = %x(identify -format '%w %h' '#{file}')
result.split(" ").map(&:to_i)
end
end
class Deck
attr_accessor :name, :cards, :card_width, :card_height, :back_image, :hidden_card_image, :remove_intermediates
attr_accessor :columns, :rows
def initialize(name:, cards:, card_width:, card_height:, back_image:, hidden_card_image: nil, remove_intermediates: true)
@name = name
@cards = cards
@card_width = card_width
@card_height = card_height
@back_image = back_image
@hidden_card_image = hidden_card_image
@remove_intermediates = remove_intermediates
find_layout!
end
def build
intermediates = []
total = cards.length
final_name = "#{name}.png"
vert_command = ["convert"]
(1..rows).each do |row|
# create image for row, appending all images
row_name = "#{name}-row-#{row}.png"
intermediates << row_name
horz_command = ["convert"]
horz_command.concat(cards.slice!(0, columns))
horz_command << "+append"
horz_command << "-depth"
horz_command << "8"
horz_command << row_name
vert_command << horz_command[-1]
run_system(horz_command)
end
# create the deck image appending all row images
if rows == 1
vert_command.concat %W(-background white -gravity south -splice 0x#{card_height})
else
vert_command.concat %w(-append -background white)
end
horz_name = "#{name}-deck.png"
vert_command << horz_name
run_system(vert_command)
if hidden_card_image
hidden_command = ["convert"]
hidden_command << horz_name
hidden_command << hidden_card_image
hidden_command.concat %w(-gravity southeast -compose over -composite)
hidden_command << final_name
run_system(hidden_command)
else
run_system(["mv", horz_name, final_name])
end
run_system(["cp", back_image, back_image_name])
run_system(["rm", *intermediates]) if remove_intermediates
puts "Deck built. Now open Tabletop Simulator, choose Objects -> Components -> Cards -> Custom Deck:"
puts "-" * 60
puts "Face: #{name}.png"
puts "Unique backs: No"
puts "Back: #{back_image_name}"
puts "Width: #{columns}"
puts "Height: #{rows}"
puts "Number: #{total}"
puts "Sideways: <only you can tell>"
puts "Back is Hidden: #{hidden_card_image ? "No" : "Yes"}"
puts "-" * 60
end
private
def back_image_name
"#{name}_back.png"
end
def find_layout!
@columns, @rows = find_factors(cards.length + (hidden_card_image ? 1 : 0))
end
# Finds the factorization of a number that provides the most balanced width and height.
# Rectangle is constraint to 2..10 x 2..7.
def find_factors(number)
return [2,2] if number <= 4
factors = []
(2..number/2).each do |n|
if number % n == 0
factors << [n, number / n]
end
end
factors.each do |fac|
fac.sort!
end
factors.sort!.uniq!&.reject! { |e| e[0] < 2 || e[0] > 7 || e[1] > 10 }
return factors[-1].reverse if factors.any?
return find_factors(number+1)
end
def run_system(command)
unless system(*command)
raise "Error: $?"
end
end
end
# The following call expects that
# - there is a directory named "Images" with a couple of Token_* images (see arguments)
# - there is a directory named "Decks"
# Output will be written to "Decks"
TableTopDeckBuilder.new(
name: "Decks/Deck_Token_RKDH_PA",
glob: "Images/Token_RKDH_PA_??.png",
back_image: "Images/Token_PA_back.png",
hidden_card_image: nil,
remove_intermediates: true
).build
# Same as above but this time the there is another hidden image used for faced up cards in other players' hands.
TableTopDeckBuilder.new(
name: "Decks/Deck_With_Hidden_Token_RKDH_PA",
glob: "Images/Token_RKDH_PA_??.png",
back_image: "Images/Token_PA_back.png",
hidden_card_image: "Images/Token_PA_hidden.png",
remove_intermediates: true
).build
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment