#!/usr/bin/ruby SERIES_DIR = "lists" APP_NAME = "The Ultimate Star Trek Slash Pairing Generator" SHOW_ADS = true SERIES = [ ["TOS", "Star Trek: The Original Series"], ["TNG", "Star Trek: The Next Generation"], ["DS9", "Star Trek: Deep Space Nine"], ["VOY", "Star Trek: Voyager"], ["ENT", "Star Trek: Enterprise"]] any = SERIES.collect { |x| x[0] }.join("|") SERIES.insert(0, [any, "every Star Trek series"]) class WeightedList < Hash def choose sum = inject(0) do |sum, item_and_weight| sum += item_and_weight[1] end target = rand(sum) each do |item, weight| return item if target <= weight target -= weight end return nil end end require 'cgi' class SlashCGI @@max_commands = 5 @@max_nsome_per_command = 3 #Build weighted list of story lengths @@story_lengths = WeightedList.new @@story_lengths["story"] = 5 ["short story", "epic", "short-short"].each do |i| @@story_lengths[i] = 3 end (2..14).each { |i| @@story_lengths["#{i}-part serial"] = 1 } #Build non-weighted list of story types. @@story_types = ["the romantic entanglements %s", "the torrid love affair %s", "the Platonic friendship %s", "the kinky depravity of", "the star-crossed love %s", "the angst-filled soul-searching of", "the innermost secrets of", "the playful bickering %s", "the unresolved sexual tension %s", "the shocking perversions of", "the troubled relationship %s", "the forbidden love %s"] #List of genders. @@genders = [["A", "(any gender)"], ["M", "male"], ["F", "female"], ["N", "ungendered/other"] ] #List of character tiers @@tiers = [["0", "primary"], ["1", "primary or secondary"], ["2", "primary, secondary, or tertiary"]] def initialize @cgi = CGI.new("html4") valid = true begin commands = gather rescue UnsatisfiableException valid = false commands = [] end @cgi.out do @cgi.html do @cgi.head do @cgi.title { APP_NAME } end + @cgi.body do a = do_ads a << @cgi.h1 { APP_NAME } unless commands.empty? begin a << make_choices(commands) a << @cgi.p { "Care to try again?" } rescue UnsatisfiableException a << "Sorry, I couldn't find a set of characters that matched your query. Try expanding your search to include more character types or more genders; or ask for fewer people." end end a << @cgi.hr a << form(commands) a << @cgi.hr a << @cgi.p { @cgi.small { %{Star Trek and related names are trademarks of Paramount Pictures, and are used under "fair use" guidelines. #{APP_NAME} is not affiliated with or sponsored by the Memory Alpha wiki or Paramount Pictures. Hoo, boy, is it not affiliated with or sponsored by Paramount Pictures. } } } a end end end end # Plaster ads on every flat surface. def do_ads return SHOW_ADS ? "" : "" end # Gather commands from CGI input def gather commands = [] 0.upto(@@max_commands) do |i| number = @cgi["#{i}-number"].to_i gender = @cgi["#{i}-gender"] series = @cgi["#{i}-series"].split("|") tier = @cgi["#{i}-tier"] # error checking: max number, bad series/gender/tier raise UnsatisfiableException if number > @@max_nsome_per_command raise UnsatisfiableException if series.detect { |x| !SERIES.detect { |y| y[0] == x }} if !series.empty? and number != "" and gender != "" and tier != "" commands << [series, number, gender, tier.to_i] if number > 0 end end return commands end def make_choices(commands) first = true choices = choose(commands) story_type = @@story_types[rand(@@story_types.size)] total = 0 # between or among? choices.each do |series, people| total += people.length break if total > 2 end if total > 2 commas = true story_type = story_type % "between" if story_type.index("%s") else commas = false story_type = story_type % "among" if story_type.index("%s") end a = @cgi.a({"href"=>"./"}) { APP_NAME } + " has spoken!

The Internet eagerly awaits your #{@@story_lengths.choose} about " if total == 1 choices.each do |series, people| a << series.link(*people[0]) end a << "." else a << story_type << " " choices.each_with_index do |sp, i| series, people = sp people.each_with_index do |p, j| last = (i == choices.size-1) && (j == people.size-1) if first first = false else a << "and " if last end a << series.link(*p) << (last ? "." : (commas ? ", " : " ")) end end end return a end def form(commands) return @cgi.form("get", "slash.cgi") do possible_nsomes = (0..@@max_nsome_per_command).collect { |x| x.to_s } o = " Pair " @@max_commands.times do |i| tiers = @@tiers.dup if commands.size > i series_ids, number, gender, tier = commands[i] selected_series = series_ids.join('|') series = SERIES.inject([]) do |build, series| if series[0] == selected_series build << (series.dup << true) else build << series end end nsomes = possible_nsomes.dup nsomes[number] = [nsomes[number], true] genders = @@genders.inject([]) do |build, g| if g[0] == gender build << (g.dup << true) else build << g end end tiers[tier] = (tiers[tier].dup << true) else series = SERIES tiers[1] = (@@tiers[1].dup << true) genders = @@genders nsomes = possible_nsomes # By default, only the first pairing argument is actually set. if i==0 nsomes = possible_nsomes.dup nsomes[2] = [nsomes[2], true] end end # puts "Series #{series.inspect}" # puts "nsomes #{nsomes.inspect}" # puts "genders #{genders.inspect}" # puts "tiers #{tiers.inspect}" o << ((i == 0) ? "" : @cgi.br + "and ") + @cgi.popup_menu("#{i}-number", *nsomes) + " of the " + @cgi.popup_menu("#{i}-gender", *genders) + @cgi.popup_menu("#{i}-tier", *tiers) + " characters from " + @cgi.popup_menu("#{i}-series", *series) end o + @cgi.br + @cgi.submit("/") end end def choose(selections, series_dir=SERIES_DIR) @series = {} choices_by_series = {} selections.each do |series_names, number, gender, bottom_tier| tiers = nil old_series = series = nil number.times do series_name = series_names[rand(series_names.size)-1] if old_series != series_name and SERIES.detect { |x| x[0] == series_name }: @series[series_name] ||= Series.new(open(File.join(series_dir, series_name))) old_series = series_name series = @series[series_name] tiers = series.gather_tiers(gender, bottom_tier) else end tier = tiers.choose raise UnsatisfiableException unless tier choice = tier.choose!(choices_by_series, gender) (choices_by_series[series] ||= []) << choice tiers.delete(tier) unless tier.can_satisfy(gender) end end choices_by_series end end class UnsatisfiableException < Exception end class Tier attr_accessor :people, :people_by_gender, :totals_by_gender def initialize @people_by_gender = {} end def add(person, sexes) if sexes.respond_to? :each sexes.each do |s| p = [person[0] % Series.genders[s].downcase, person[1]] (@people_by_gender[s] ||= []) << p (@people_by_gender["A"] ||= []) << p end else (@people_by_gender[sexes] ||= []) << person (@people_by_gender["A"] ||= []) << person end end # Chooses a person of the given gender from this tier, and removes them # from the tier. def choose!(choices, gender) raise UnsatisfiableException unless can_satisfy(gender) people = @people_by_gender[gender] i = rand(people.size) choice = nil while !choice or choices.detect { |series| choices[series] and choices[series].detect { |x| x[0] == choice[0] } } choice = people.slice!(i) end return choice end def can_satisfy(gender) return @people_by_gender[gender] && !@people_by_gender[gender].empty? end end class Series def Series.genders @@genders end def wikify(person, page) page = person.gsub(" ", "_") unless page if page.strip.size > 0 return @base_url % page else return nil end end def link(person, page) link = wikify(person, page) if link != @base_url person = %{#{person}} end return person end @@probabilities = [45, 33, 22] @@genders = { "M" => "Male", "F" => "Female", "N" => "Other" } def initialize(file) @base_url = file.readline.chomp @tiers = [] working_gender = nil @tiers << (working_tier = Tier.new()) file.each do |line| line = line.chomp if line.size == 0 @tiers << (working_tier = Tier.new()) else g = line[0].chr if line[1] == ?\s and (@@genders[g] || g == "E") working_gender = g == "E" ? ["M", "F"] : g line = line[2..line.size] end l = line.split("|") page = l.size > 1 ? l[1] : nil working_tier.add([l[0], page], working_gender) end end end def gather_tiers(must_have_gender, bottom) list = WeightedList.new found = 0 0.upto([bottom,@tiers.size-1].min) do |i| tier = @tiers[i] if tier.can_satisfy(must_have_gender) list[tier] = @@probabilities[found] found += 1 end end return list end end SlashCGI.new