#!/usr/bin/env ruby

# The Castlevania 2: Simon's Quest (NES) password generator.
#
# See http://www.gamefaqs.com/nes/587179-castlevania-ii-simons-quest/faqs/38582
#
# All method comments cite that faq as well.
#
# For those unfamiliar with ruby, grab it at http://www.ruby-lang.org/en/downloads/ .
# Once installed, use the command prompt to run this script
# $ ruby /whereever/you/put/castlevania2 --garlics 3 --laurels 7
# OFKT KPYX
# WH4S RMAA
#
# Pass --help to see what you can do.
#
# It goes without saying do whatever you want with this.  Don't trust it with
# the launch codes or the nuclear reactor.
class Castlevania2Password

  def initialize(options = {})
    options.each { |k, v| send :"#{k}=", v }
  end

  # 'Use' items, boolean
  attr_accessor :holy_water, :diamond, :holy_flame, :oak_stake

  # Dagger item.  One of nil, :plain, :silver, or :golden
  attr_accessor :dagger

  # Quantified items, Integer 0..8
  attr_accessor :laurels, :garlics

  # Time in game days.  Integer 0..9
  attr_accessor :day
  
  # Simon's level.  Integer 1..6
  attr_accessor :level
  
  # 'Quest' and other items, boolean
  attr_accessor :rib, :heart, :eye, :nail, :ring, :magic_cross, :silk_bag
  
  # Crystal item.  One of nil, :white, :blue, or :red
  attr_accessor :crystal
  
  # Whip "level".  Integer 0..4
  attr_accessor :whip

  # The frame counter used to salt generated passwords. Integer 0..15
  #
  # Note that this value actually varies up to 255, but only the lower 4 bits
  # are ever used.
  attr_accessor :frame

  def inspect
    args = [:holy_water, :diamond, :holy_flame, :oak_stake, :dagger, :laurels,
      :garlics, :day, :level, :rib, :heart, :eye, :nail, :ring, :magic_cross,
      :silk_bag, :crystal, :whip, :frame].select do |p|
      send p
    end.collect do |p|
      ":#{p} => #{send(p).inspect}"
    end.join(", ")
    
    "#{self.class.name}.new(#{args})"
  end

  # The password.
  # 
  # From the faq:
  # Finally, to get the actual bytes that correspond to the actual
  # password, add 1 to each of the bytes in the range $0530-$053F.
  # Store these values at $0540-$054F.
  # 
  #     for(i = 0x00; i <= 0x0f; i++) {
  #         $054i = $053i + 1;
  #     }
  # 
  # Now, all we need to know is the correspondence between the bytes
  # in $0540-$054F and the characters used in the password. Here is 
  # a table that shows the correspondence:
  # 
  #     A - 0x01     K - 0x0B     U - 0x15     4 - 0x1F
  #     B - 0x02     L - 0x0C     V - 0x16     5 - 0x20
  #     C - 0x03     M - 0x0D     W - 0x17     6 - 0x21
  #     D - 0x04     N - 0x0E     X - 0x18     7 - 0x22
  #     E - 0x05     O - 0x0F     Y - 0x19     8 - 0x23
  #     F - 0x06     P - 0x10     Z - 0x1A     9 - 0x24
  #     G - 0x07     Q - 0x11     0 - 0x1B
  #     H - 0x08     R - 0x12     1 - 0x1C
  #     I - 0x09     S - 0x13     2 - 0x1D
  #     J - 0x0A     T - 0x14     3 - 0x1E
  # 
  # The final password is the 16 characters given by the
  # correspondence in the table above in the same order as the bytes
  # in $0540 - $054F.
  #
  # We skip the addition and used a 0-based index lookup.
  def password
    substitution_bytes.collect do |byte|
      'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[byte..byte]
    end.join.gsub /(.{4})(.{4})(.{4})(.{4})/, "\\1 \\2\n\\3 \\4"
  end
  
  alias_method :to_s, :password

private

  # Group an array of at most 8 booleans into a byte.
  #
  # If less than 8 booleans are passed, the missing bits are left 0 at the high end.
  def pack(*flags)
    raise ArgumentError.new("8 bits in a byte, bub") unless flags.size <= 8
    byte = 0
    flags.each_with_index { |flag, i| byte |= (1 << i) if flag }
    byte
  end

  # Check type and bounds for integer properties.
  #
  # If the property has not been set, assume the minimum.
  def assert_integer(name, range)
    value = send(name) || range.min
    unless value.is_a? Fixnum and range.include? value
      raise ArgumentError.new("#{name} must be between #{range.min} and #{range.max}")
    end
    value
  end

  # Set the dagger bit?
  def plain_dagger?
    golden_dagger? or silver_dagger? or dagger == :plain
  end

  # Set the silver dagger bit?
  def silver_dagger?
    golden_dagger? or dagger == :silver
  end

  # Set the golden dagger bit?
  def golden_dagger?
    dagger == :golden
  end

  # Set the white crystal bit?
  def white_crystal?
    crystal == :white or crystal == :red
  end

  # Set the blue crystal bit?
  def blue_crystal?
    crystal == :blue or crystal == :red
  end

  # Set the laurel bit?
  def laurel?
    !!laurels
  end

  # Set the garlic bit?
  def garlic?
    !!garlics
  end

  # The byte describing 'use' items.
  #
  # From the faq:
  # $004A - use items
  #         bits (from highest order to lowest)
  #             7 - not used, keep set to 0
  #             6 - Oak Stake
  #             5 - Holy Flame
  #             4 - Diamond
  #             3 - Holy Water
  #             2 - Golden Dagger
  #             1 - Silver Dagger
  #             0 - Dagger
  def use_item_byte
    pack plain_dagger?, silver_dagger?, golden_dagger?, holy_water, diamond, holy_flame, oak_stake
  end

  # The byte describing the garlic and laurel counts.
  #
  # From the faq:
  # $004D - quantity of garlic (max. 8)
  # ...
  # $004C - quantity of laurels (max. 8)
  # ...
  # $0522 = $4C | $4D << 4    ; # of garlic and laurels (see note below)
  # ...
  # note: # of garlic is stored in the upper 4 bytes of $522 whereas # of
  #       laurels is stored in the lower 4 bytes
  #
  # In the final sentence "bytes" is taken as mistaken for "bits".
  def garlic_and_laurel_byte
    assert_integer(:laurels, 0..8) | assert_integer(:garlics, 0..8) << 4
  end

  # The byte describing the days elapsed.
  #
  # From the faq:
  # $0083 - time elapsed in game days (max. 9)
  def day_byte
    assert_integer :day, 0..9
  end

  # The byte describing Simon's level.
  #
  # From the faq:
  # $008B - Simon's level (max. 6)
  def level_byte
    assert_integer :level, 1..6
  end

  # The byte describing 'quest' items.
  #
  # From the faq:
  # $0091 - quest items
  #         bits (from highest order to lowest)
  #             7 - not used, keep set to 0
  #             6 - Blue Crystal    ---+
  #                                    +--- set both to get the Red Crystal
  #             5 - White Crystal   ---+
  #             4 - Dracula's Ring
  #             3 - Dracula's Nail
  #             2 - Dracula's Eye
  #             1 - Dracula's Heart
  #             0 - Dracula's Rib
  def quest_item_byte
    pack rib, heart, eye, nail, ring, white_crystal?, blue_crystal?
  end

  # The byte describing 'other' items.
  #
  # From the faq:
  # $0092 - other items
  #         bits (from highest order to lowest)
  #             7-4 - not used, keep set to 0
  #             3 - Garlic
  #             2 - Laurels
  #             1 - Magic Cross
  #             0 - Silk Bag
  def other_item_byte
    pack silk_bag, magic_cross, laurel?, garlic?
  end

  # The byte describing the whip.
  #
  # From the faq:
  # $0434 - whip (takes on values between 0 and 4)
  #             0 - Leather Whip
  #             1 - Thorn. Whip
  #             2 - Chain Whip
  #             3 - Morning Star
  #             4 - Flame Whip
  def whip_byte
    assert_integer :whip, 0..4
  end

  # The seven password generation content bytes (as an array).
  #
  # From the faq:
  # Now we can begin with the generation algorithm. The password generation code
  # begins at $AF21 in RAM. First, the game stores the contents of all the 
  # above addresses into the address range $0520-$0526. It does this in the 
  # following manner:
  # 
  #     $0520 = $8B    ; Simon's experience level
  #     $0521 = $83    ; Time elapsed in game days
  #     $0522 = $4C | $4D << 4    ; # of garlic and laurels (see note below)
  #     $0523 = $91    ; quest items
  #     $0524 = $92    ; other items
  #     $0525 = $4A    ; use items
  #     $0526 = $0434  ; whip
  def content_bytes
    [level_byte, day_byte, garlic_and_laurel_byte, quest_item_byte, other_item_byte, use_item_byte, whip_byte]
  end

  # The two checksum bytes (as an array) of the passed content bytes.
  #
  # From the faq:
  # Next, checksums are computed and stored in $0527-$0528.
  # 
  #     $0527 = ($0520 + $0521 + $0522 + $0523 + $0524 + $0525 + $0526) & 0xFF00
  #     $0528 = ($0520 + $0521 + $0522 + $0523 + $0524 + $0525 + $0526) & 0x00FF
  #
  # $0527 is taken as also >> 8 after masking.
  def checksum_bytes(content)
    total = content.inject(0) { |sum, byte| sum + byte }
    [(total & 0xff00) >> 8, total & 0x00ff]
  end

  # The frame counter byte used to salt generated passwords.
  #
  # From the faq:
  # For randomization, the password generated uses a value from a counter that 
  # updates on every frame (the counter increments by 0x01 and goes through all
  # values 0x00-0xFF). The address of this counter is $1D.
  def frame_counter_byte
    # assert_integer :frame, 0..255
    assert_integer :frame, 0..15
  end

  # The lower 4 bits of #frame_counter_byte.
  #
  # From the faq:
  # The lower 4 bytes of $0529 is the same as the lower 4 bytes of the counter
  # value.
  def frame_counter_nibble
    frame_counter_byte & 0x0f
  end

  # The lookup table that #frame_counter_byte indexes.
  #
  # From the faq:
  # The randomizer uses a lookup table for the upper 4 bytes of the $0529 byte.
  # The lower 4 bytes of $0529 is the same as the lower 4 bytes of the counter
  # value. Only a single counter value is used for the calculation of $04F7 and
  # $04F6 values.
  # 
  # The lookup table begins at $AF19 and is 8 bytes long.
  # 
  #     $AF19 - $AF20
  #     $AF10: xx xx xx xx xx xx xx xx xx 00 01 02 03 04 01 02 
  #     $AF20: 03 xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx
  def frame_counter_lookup_nibble
    [0, 1, 2, 3, 4, 1, 2, 3][frame_counter_byte & 0x07]
  end

  # The byte used to salt generated passwords.
  #
  # From the faq:
  # A "random" value 
  # used in password generation is stored in $0529 after some intermediate 
  # calculations, which are stored at $04F7 and $04F6.
  # ...
  # Now for the randomization value code.
  # 
  #     $04F7 = $(0xAF19 + $1D & 0x07)
  #     $04F6 = $1D & 0x0F
  #     $0529 = $04F6 | $04F7 << 4
  def salt_byte
    frame_counter_nibble | frame_counter_lookup_nibble << 4
  end

  # The content, checksum and salt bytes together.
  #
  # We call it the plaintext, since the next two steps operate on it in a
  # way reminiscent of a 1-layer permutation-substitution network.
  def plaintext_bytes
    (content = content_bytes) + checksum_bytes(content) + [salt_byte]
  end

  # The plaintext after a series of logical operations ("permutations").
  #
  # From the faq:
  # Next, a series of logical operations are performed on the bytes $0520-$0529
  # to generate the bytes $0530-$053F.
  # 
  #     $0530 = $0520 >> 3
  #     $0531 = ($0520 & 0x07) << 2 | $0521 >> 6
  #     $0532 = ($0521 & 0x3E) >> 1
  #     $0533 = ($0521 & 0x01) << 4 | $0522 >> 4
  #     $0534 = ($0522 & 0x0F) << 1 | $0523 >> 7
  #     $0535 = ($0523 & 0x7C) >> 2
  #     $0536 = ($0523 & 0x03) << 3 | ($0524 & 0xE0) >> 5
  #     $0537 = $0524 & 0x1F
  #     $0538 = $0525 >> 3
  #     $0539 = ($0525 & 0x07) << 2 | $0526 >> 6
  #     $053A = ($0526 & 0x3E) >> 1
  #     $053B = ($0526 & 0x01) << 4 | $0527 >> 4
  #     $053C = ($0527 & 0x0F) << 1 | $0528 >> 7
  #     $053D = ($0528 & 0x7C) >> 2
  #     $053E = ($0528 & 0x03) << 3 | $0529 >> 5
  #     $053F = $0529 & 0x1F
  def permutation_bytes
    plaintext = plaintext_bytes
    [
      (plaintext[0] >> 3),
      (plaintext[0] & 0x07) << 2 | (plaintext[1] >> 6),
      (plaintext[1] & 0x3e) >> 1,
      (plaintext[1] & 0x01) << 4 | (plaintext[2] >> 4),
      (plaintext[2] & 0x0f) << 1 | (plaintext[3] >> 7),
      (plaintext[3] & 0x7c) >> 2,
      (plaintext[3] & 0x03) << 3 | (plaintext[4] & 0xe0) >> 5,
      (plaintext[4] & 0x1f),
      (plaintext[5] >> 3),
      (plaintext[5] & 0x07) << 2 | (plaintext[6] >> 6),
      (plaintext[6] & 0x3e) >> 1,
      (plaintext[6] & 0x01) << 4 | (plaintext[7] >> 4),
      (plaintext[7] & 0x0f) << 1 | (plaintext[8] >> 7),
      (plaintext[8] & 0x7c) >> 2,
      (plaintext[8] & 0x03) << 3 | (plaintext[9] >> 5),
      (plaintext[9] & 0x1f)
    ]
  end

  # The XOR mask used in the "substitution" step.
  #
  # From the faq:
  # Here, another look-up table is used -- this 
  # one containing 2-byte memory address stored with the lower-order 
  # byte first. Start at $B228 and add to the address itself the value 
  # at $04F6 shifted left by 1.  The address found in the look-up table
  # is stored at $10 and $11 with the same byte order. The table begins
  # at $B228 and is 32 bytes long.
  #
  # $B228 - $B247
  # $B220: xx xx xx xx xx xx xx xx 48 B2 56 B2 64 B2 72 B2 
  # $B230: 80 B2 8E B2 9C B2 AA B2 B8 B2 C6 B2 D4 B2 E2 B2
  # $B240: F0 B2 FE B2 0C B3 1A B3 xx xx xx xx xx xx xx xx
  #
  # At each address listed in the table above is a series of 14 bytes
  # used to XOR with the data at values $0530-$053D. 
  # 
  #     $B248 - 0E 01 0A 10 04 0F 18 1B 16 07 1E 12 11 1D
  #     $B256 - 04 0F 18 1B 16 07 1E 12 0B 12 02 03 11 1D
  #     $B264 - 0A 01 0E 10 04 0C 18 1B 16 07 0A 12 11 1D
  #     $B272 - 0E 0D 0A 1C 04 0D 1A 1B 06 07 1E 12 15 1D
  #     $B280 - 0E 11 0A 10 04 03 18 1B 16 07 12 12 11 1D
  #     $B28E - 13 09 0A 12 04 03 18 1B 12 07 16 0A 10 15
  #     $B29C - 02 0D 0A 12 04 0F 0A 1B 12 07 18 12 11 1D
  #     $B2AA - 00 1D 0A 18 04 0B 18 1A 14 07 1E 14 11 1D
  #     $B2B8 - 0E 0D 0E 12 0C 0F 1A 1B 16 07 1E 12 15 1D
  #     $B2C6 - 0E 01 0A 10 04 0F 18 1B 14 07 00 12 11 1D
  #     $B2D4 - 00 01 02 03 04 05 06 07 08 09 0A 0B 11 1D
  #     $B2E2 - 10 0D 0A 16 04 0F 18 1B 16 07 1E 12 1D 1D
  #     $B2F0 - 02 03 0A 12 04 0F 18 0B 16 07 06 16 01 1D
  #     $B2FE - 18 05 0A 1A 04 0D 18 1B 16 03 1A 16 17 15
  #     $B30C - 03 05 0A 02 04 0D 08 0F 1E 0B 12 16 11 04
  #     $B31A - 1E 05 0A 1A 07 0D 1A 1F 17 00 1A 16 16 15
  def substitution_xor_bytes
    address = [0xb248, 0xb256, 0xb264, 0xb272, 0xb280, 0xb28e, 0xb29c, 0xb2aa,
      0xb2b8, 0xb2c6, 0xb2d4, 0xb2e2, 0xb2f0, 0xb2fe, 0xb30c, 0xb31a][frame_counter_nibble]

    [
      [0x0E, 0x01, 0x0A, 0x10, 0x04, 0x0F, 0x18, 0x1B, 0x16, 0x07, 0x1E, 0x12, 0x11, 0x1D],
      [0x04, 0x0F, 0x18, 0x1B, 0x16, 0x07, 0x1E, 0x12, 0x0B, 0x12, 0x02, 0x03, 0x11, 0x1D],
      [0x0A, 0x01, 0x0E, 0x10, 0x04, 0x0C, 0x18, 0x1B, 0x16, 0x07, 0x0A, 0x12, 0x11, 0x1D],
      [0x0E, 0x0D, 0x0A, 0x1C, 0x04, 0x0D, 0x1A, 0x1B, 0x06, 0x07, 0x1E, 0x12, 0x15, 0x1D],
      [0x0E, 0x11, 0x0A, 0x10, 0x04, 0x03, 0x18, 0x1B, 0x16, 0x07, 0x12, 0x12, 0x11, 0x1D],
      [0x13, 0x09, 0x0A, 0x12, 0x04, 0x03, 0x18, 0x1B, 0x12, 0x07, 0x16, 0x0A, 0x10, 0x15],
      [0x02, 0x0D, 0x0A, 0x12, 0x04, 0x0F, 0x0A, 0x1B, 0x12, 0x07, 0x18, 0x12, 0x11, 0x1D],
      [0x00, 0x1D, 0x0A, 0x18, 0x04, 0x0B, 0x18, 0x1A, 0x14, 0x07, 0x1E, 0x14, 0x11, 0x1D],
      [0x0E, 0x0D, 0x0E, 0x12, 0x0C, 0x0F, 0x1A, 0x1B, 0x16, 0x07, 0x1E, 0x12, 0x15, 0x1D],
      [0x0E, 0x01, 0x0A, 0x10, 0x04, 0x0F, 0x18, 0x1B, 0x14, 0x07, 0x00, 0x12, 0x11, 0x1D],
      [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x11, 0x1D],
      [0x10, 0x0D, 0x0A, 0x16, 0x04, 0x0F, 0x18, 0x1B, 0x16, 0x07, 0x1E, 0x12, 0x1D, 0x1D],
      [0x02, 0x03, 0x0A, 0x12, 0x04, 0x0F, 0x18, 0x0B, 0x16, 0x07, 0x06, 0x16, 0x01, 0x1D],
      [0x18, 0x05, 0x0A, 0x1A, 0x04, 0x0D, 0x18, 0x1B, 0x16, 0x03, 0x1A, 0x16, 0x17, 0x15],
      [0x03, 0x05, 0x0A, 0x02, 0x04, 0x0D, 0x08, 0x0F, 0x1E, 0x0B, 0x12, 0x16, 0x11, 0x04],
      [0x1E, 0x05, 0x0A, 0x1A, 0x07, 0x0D, 0x1A, 0x1F, 0x17, 0x00, 0x1A, 0x16, 0x16, 0x15]
    ][(address - 0xb248) / 14]
  end

  # The "permutation" bytes after applying the substituion XOR mask.
  #
  # From the faq:
  # Next, the data in $0530-$53F is modified based on the random values
  # generated previously.
  # ...
  # The above is done by:
  # 
  #     for(i = 0x00; i < 0x0e; i++) {
  #         $10 = $(0xB228 + ($04F6 << 1))
  #         $11 = $(0xB229 + ($04F6 << 1))
  #         $053i = $053i ^ $($10 + ($11 << 2) + i)
  #     }
  def substitution_bytes
    permutation_bytes[0..13].zip(substitution_xor_bytes).collect do |permuted, mask|
      permuted ^ mask
    end + permutation_bytes[14..15]
  end

end

# If run from the command line
if __FILE__ == $0
  require 'optparse'

  password = Castlevania2Password.new

  OptionParser.new do |opts|
    opts.banner = "Usage: castlevania2 [options]"
    opts.separator ""
    opts.separator "Options:"

    opts.on_tail("-h", "--help", "Show this message") do
      puts opts
      exit
    end

    [:level, :whip].each do |o|
      opts.on("--#{o.to_s.tr('_', '-')} #{o.to_s[0..0].upcase}", Integer,
        "Start with #{o.to_s.tr('_', ' ')} #{o.to_s[0..0].upcase}") do |n|
        password.send :"#{o}=", n
      end
    end

    [:laurels, :garlics, :day].each do |o|
      opts.on("--#{o.to_s.tr('_', '-')} #{o.to_s[0..0].upcase}", Integer,
        "Start with #{o.to_s[0..0].upcase} #{o.to_s.tr('_', ' ')}") do |n|
        password.send :"#{o}=", n
      end
    end

    [:holy_water, :diamond, :holy_flame, :oak_stake, :rib, :heart,
      :eye, :nail, :ring, :magic_cross, :silk_bag].each do |o|
      opts.on("--#{o.to_s.tr('_', '-')}", "Start with the #{o.to_s.tr('_', ' ')}") do
        password.send :"#{o}=", true
      end
    end

    [:dagger, :silver_dagger, :golden_dagger].each do |o|
      opts.on("--#{o.to_s.tr('_', '-')}", "Start with the #{o.to_s.tr('_', ' ')}") do
        flavor = o.to_s.split('_').first
        password.dagger = flavor == 'dagger' ? :plain : flavor.to_sym
      end
    end

    [:white_crystal, :blue_crystal, :red_crystal].each do |o|
      opts.on("--#{o.to_s.tr('_', '-')}", "Start with the #{o.to_s.tr('_', ' ')}") do
        password.crystal = o.to_s.split('_').first.to_sym
      end
    end

    opts.on("--frame F", Integer, "Randomize with frame F (you don't care)") do |n|
      password.frame = n
    end
    
  end.parse! ARGV
  
  begin
    puts password
  rescue ArgumentError => e
    warn "Error: #{e}"
    exit 1
  end
end
