So about a year and a half ago, me and my friend started on a game called Zombie Smack Down. It's a text based game written in Ruby. But we were both really new to Ruby when most of it was written. I realize lots of this code is probably really bad, but I'm kinda stuck in my ways. I'd love to know which parts of it can be refactored, and changed to follow Ruby best practices, in the game and in the tests The code is also on Github here, but that'll probably get updated and no longer be the same as the code on this site. (and if you find any camel case methods, or variables please let me know, but I tried to remove all that, but I may have missed some. I know it's bad practice in Ruby, I was younger when I made much of this.) If you install if from the one-liner on github, you don't have to enter your correct password, but if you do it'll add a really outdate manual page. Sorry, the installer and updater don't work on Windows and probably not Linux either. I don't have a way to test that.And I know a lot of this can be updated for Ruby 2 syntax, but I need it to be compatible with the computers at my school, which have 1.8.7 installed, and jailbroken iPods, using Ruby 1.8.6 which is why there a lot of parenthesis around things like puts, because it gives warnings. For now, the tests only really need to work in Ruby 2.1.1, I'm not even sure how well it works in 1.8.7/1.8.6One last thing, the signal trapping is not working, I tried typing Ctrl-c and it didn't quit. I'd love help with that, but I'm asking this question for a Code Review. It's just a side note.requie/player.yml, scores.yml, and tests.rb are on Github only because it said I had too many charecters in my question.zsd:#!/usr/bin/env ruby$rpath = #{File.expand_path '~'}/.zsdrequire optparse$options = []OptionParser.new { |opts|    opts.on(-i, --info, Shows info on current game) {        $options << i    }    opts.on(-h, --help, Shows help page) {        $options << h    }    opts.on(-s, --scores, Shows top 5 high scores) {        $options << s    }    opts.on(-u, --update, Updates zsd) {        $options << u    }    opts.on(-t, --testing, For dev use only) {        $rpath = .    }}.parse!require #{$rpath}/require/otherdef main    cli = Cli.new    unless $options.empty?        $options.each { |o|            case o            when i                cli.player.info                exit            when h                cli.tutorial                exit            when s                cli.scores                exit            when u                system ~/.zsd/update.sh                exit            end        }    end    %w[ TERM INT HUP ].each { |signal|        trap(signal) {            exit_game $player        }    }    start clienddef start cli    p_info(Type help for help.)    loop {        cli.spawn_zombie        while cli.zombie.is_alive            input = prompt(>).split            command = input[0]            if cli.commands.include? command                cli.send(command, *input[1..-1])            else                p_warn(Unknown Command.) unless command.nil?            end        end # zombie has died if loop ends    } # end game loopendif __FILE__ == $0    mainendrequire/colors.rb:class String    def black;          \033[30m#{self}\033[0m end    def red;            \033[31m#{self}\033[0m end    def green;          \033[32m#{self}\033[0m end    def brown;          \033[33m#{self}\033[0m end    def blue;           \033[34m#{self}\033[0m end    def magenta;        \033[35m#{self}\033[0m end    def cyan;           \033[36m#{self}\033[0m end    def gray;           \033[37m#{self}\033[0m end    def bg_black;       \033[40m#{self}\033[0m end    def bg_red;         \033[41m#{self}\033[0m end    def bg_green;       \033[42m#{self}\033[0m end    def bg_brown;       \033[43m#{self}\033[0m end    def bg_blue;        \033[44m#{self}\033[0m end    def bg_magenta;     \033[45m#{self}\033[0m end    def bg_cyan;        \033[46m#{self}\033[0m end    def bg_gray;        \033[47m#{self}\033[0m end    def bold;           \033[1m#{self}\033[22m end    def reverse_color;  \033[7m#{self}\033[27m endenddef p_info s    puts s.magentaenddef p_error s    puts s.redenddef p_pain s    puts s.redenddef p_level_up s    puts s.cyanenddef p_warn s    puts s.brown # its yellowendrequire/other.rb:require readlinerequire yamlrequire #{$rpath}/require/zombierequire #{$rpath}/require/colorsrequire #{$rpath}/require/comborequire #{$rpath}/require/playerclass Cli    attr_accessor :player, :zombie, :commands    def initialize         @player = Player.new        @commands = %w[ kick punch combo combolist taunt info scores quit help heal easter ]        @combos = { kick punch => KickPunch.new,                   trip stomp => TripStomp.new,                   punch punch kick => PunchPunchKick.new,                   Knee Punch Face Slap => KneePunchFaceSlap.new,                   heal fury => HealFury.new(@player),                   elbow fist knee fist knee body slam => ElbowFistKneeFistKneeBodySlam.new,                   kick kick kick kick kick kick kick kick kick kick kick kick kick kick kick => KickKickKickKickKickKickKickKickKickKickKickKickKickKickKick.new,                   combo of possible death => ComboOfPossibleDeath.new,                   combo of death => ComboOfDeath.new,                   coolest combo ever => CoolestComboEver.new,                   chase punch of fire => ChasePunchOfFire.new,                   addison kick of cold hard music => AddisonKickOfColdHardMusic.new,                   not a combo => NotACombo.new,                   pain with a side of blood => PainWithASideOfBlood.new,                   the combo => TheCombo.new,                   the 2nd combo => The2ndCombo.new,                   ultimate destruction kick punch => UltimateDestructionKickPunch.new,                   the 3rd combo => The3rdCombo.new,                   pretty good combo => PrettyGoodCombo.new,                   chuck norris stomp of mayhem => ChuckNorrisStompOfMayhem.new        }    end    def spawn_zombie        zombies = [ Zombie,                BigZombie,                DaddyZombie,                GunZombie,                NinjaZombie,                IdiotZombie,                BlindZombie,                StrongZombie,                BasicallyDeadZombie,                SuperZombie,                BossZombie,                UltimateZombie ]        @zombie = zombies[@player.save[:wave] - 1].new    end    def attack damage        @zombie.take_damage damage        z_damage = @zombie.attack        @player.take_damage z_damage if @zombie.is_alive        p_pain(#{@player.phrases.rand_choice} #{@zombie.name}! -#{damage})        p_pain(#{@zombie.name} #{@zombie.phrases.rand_choice}! -#{z_damage})        @zombie.check_dead        @player.check_dead        @player.add_kill if !@zombie.is_alive        @player.give_xp @zombie.xp if !@zombie.is_alive    end    def do_combo        # keep combos in order of xp cost        # and keep keys lowercase        if @combos.has_key? c = prompt(Which combo? )            used_combo = @combos[c]            if @player.save[:xp] >= used_combo.price                damage = used_combo.use                @player.give_xp -used_combo.price                return true, damage            else                p_warn(You don't have enough xp loser.)                return false            end        else             p_warn(That is not a combo.)            return false        end    end    # CLI METHODS BELOW    def kick *args        attack @player.kick    end    def punch *args        attack @player.punch    end    def combo *args        success, damage = do_combo        attack damage if success    end    def combolist *args        amount = @player.save[:rank]        p_info Unlocked Combos:         combos = @combos.sort_by { |k, v| v.price }        amount.times { |i|            p_info #{combos[i][1].name}: -#{combos[i][1].price} xp        }    end    def scores *args        scores = YAML.load_file(#{$rpath}/scores.yml)        p_info High Scores:        scores.each { |s|            p_info #{s[1]}: #{s[0]}        }    end    def quit *args        p_warn Wanna save yo game? yes or no        answer = prompt        while !([yes, y, no, n].include? answer)            p_warn I didn't catch that. Yes or No?            answer = prompt        end        save_game = (answer == yes ? true : false)        @player.save_game if save_game        exit    end    def help *args        p_info Available commands:        puts(@commands.join  )        end    def taunt *args        if @player.save[:taunts_available] > 0            @player.taunt        else            p_warn You have no more taunts.        end    end    def heal *args        amount = args[0].to_i        if amount > 0            @player.heal amount        else            p_warn Please specify a number greater than 0. Example: heal 5        end    end    def info *args        @player.info        puts        @zombie.info    end    def easter *args        if args[0] == egg            unless @player.save[:egg_used]                xp = (-50..75).to_a.rand_choice                @player.give_xp xp                p_level_up #{(xp >= 0 ? + : -) + xp.abs.to_s} xp                @player.save[:egg_used] = true            else                p_warn You used your easter egg this game you cheater :/            end        else            p_warn Unknown Command.        end    endendclass Array    def rand_choice        if RUBY_VERSION.to_f > 1.8            self.sample        else            self[rand(self.length)]        end    endenddef exit_game player  Thread.new {    player.save_game    puts(^C)    p_level_up Game saved.    exit  }enddef prompt _prompt=, newline=false    _prompt += \n if newline    inText = Readline.readline(_prompt, true).squeeze( ).strip.downcase    inTextendrequire/combo.rb:class Combo    attr_accessor :name, :price    def initialize player=nil        @player = player        self.setInfo    end    def use        self.extra        @damage.to_a.rand_choice    end    def extra    endend# try to keep combos in order of xp cost# also add new combos to combos variable in the start function in the zsd fileclass KickPunch < Combo    def setInfo        @name = Kick Punch        @price = 2        @damage = (3..9)    endendclass TripStomp < Combo    def setInfo        @name = Trip Stomp        @price = 3        @damage = (4..10)    endendclass PunchPunchKick < Combo    def setInfo        @name = Punch Punch Kick        @price = 4        @damage = (4..12)    endendclass KneePunchFaceSlap < Combo    def setInfo        @name = Knee Punch Face Slap        @price = 4        @damage = (2..12)    endendclass HealFury < Combo    def setInfo        @name = Heal Fury        @price = 5        @damage = (5..10)    end    def extra        health = -((3..7).to_a.rand_choice)        @player.take_damage health    endendclass ElbowFistKneeFistKneeBodySlam < Combo    def setInfo        @name = Elbow Fist Knee Fist Knee Body Slam        @price = 6        @damage = (3..18)    endendclass KickKickKickKickKickKickKickKickKickKickKickKickKickKickKick < Combo    def setInfo        @name = Kick Kick Kick Kick Kick Kick Kick Kick Kick Kick Kick Kick Kick Kick Kick        @price = 7        @damage = (9..17)    endendclass ComboOfPossibleDeath < Combo    def setInfo        @name = Combo of Possible Death        @price = 9        @damage = (5..25)    endendclass ComboOfDeath < Combo    def setInfo        @name = Combo of Death        @price = 12        @damage = (14..30)    endendclass CoolestComboEver < Combo    def setInfo        @name = Coolest Combo Ever        @price = 15        @damage = (10..25)    endendclass ChasePunchOfFire < Combo    def setInfo        @name = Chase Punch of Fire        @price = 20        @damage = (20..40)    endendclass AddisonKickOfColdHardMusic < Combo    def setInfo        @name = Addison Kick of Cold Hard Music        @price = 20        @damage = (20..40)    endendclass NotACombo < Combo    def setInfo        @name = Not A Combo        @price = 20        @damage = (25..45)    endendclass PainWithASideOfBlood < Combo    def setInfo        @name = Pain With a Side of Blood        @price = 25        @damage = (35..50)    endendclass TheCombo < Combo    def setInfo        @name = The Combo        @price = 25        @damage = (1..100)    endendclass The2ndCombo < Combo    def setInfo        @name = The 2nd Combo        @price = 30        @damage = (20..100)    endendclass UltimateDestructionKickPunch < Combo    def setInfo        @name = Ultimate Destruction Kick Punch        @price = 30        @damage = (40..75)    endendclass The3rdCombo < Combo    def setInfo        @name = The 3rd Combo        @price = 35        @damage = (50..85)    endendclass PrettyGoodCombo < Combo    def setInfo        @name = Pretty Good Combo        @price = 50        @damage = (45..111)    endendclass ChuckNorrisStompOfMayhem < Combo    def setInfo        @name = Chuck Norris Stomp of Mayhem        @price = 1000        @damage = (1..2_000_000)    endendrequire/player.rb:class Player    # try to put methods in alphebetical order, except init    attr_accessor :save, :phrases    def initialize        @save = YAML.load_file(#{$rpath}/require/player.yml)        @save_original = { :health => 25,                           :xp => 15,                           :rank => 1,                           :wave => 1,                           :zombies_killed => 0,                           :total_kills => 0,                           :kick_upgrade => 0,                           :punch_upgrade => 0,                           :taunts_available => 3,                           :egg_used => false,                           :new_game => true }        if @save[:new_game]            self.give_xp((@save[:rank] - 1) * 2)            @save[:new_game] = false        end        @phrases = [You smacked down the, You hit the, Whose your daddy, You just powned the]    end    def add_kill        @save[:total_kills] += 1        @save[:zombies_killed] += 1        self.next_wave if @save[:zombies_killed] % 3 == 0        self.rank_up if @save[:total_kills] % 15 == 0    end    def check_dead        self.die if @save[:health] <= 0    end    def die        self.reset        p_warn You died!!!        p_warn You killed #{@save[:zombies_killed]} zombies.        self.save_score        exit    end    def give_xp amount        @save[:xp] += amount    end    def heal amount        if @save[:xp] >= amount            @save[:health] += amount            self.give_xp -amount            p_level_up +#{amount} health!        else            p_warn You do not have enough xp!        end    end    def info        p_info Health: #{@save[:health]}        p_info XP: #{@save[:xp]}        p_info Rank: #{@save[:rank]}        p_info Wave: #{@save[:wave]}        p_info Zombies Killed: #{@save[:zombies_killed]}        p_info Total Kills: #{@save[:total_kills]}        p_info Kick Upgrade: #{@save[:kick_upgrade]}        p_info Punch Upgrade: #{@save[:punch_upgrade]}        p_info Taunts Available: #{@save[:taunts_available]}    end    def kick        (3..7).to_a.rand_choice + @save[:kick_upgrade]    end    def next_wave        @save[:wave] += 1        xp = @save[:wave] + 2        self.give_xp xp        p_level_up Wave #{@save[:wave]}, +#{xp} xp    end    def punch        (4..6).to_a.rand_choice + @save[:punch_upgrade]    end    def rank_up        @save[:rank] += 1        p_level_up Rank Up! You are now rank #{@save[:rank]}. You unlocked a new combo.        self.upgrade    end    def reset        @save_original[:rank] = @save[:rank]        @save_original[:total_kills] = @save[:total_kills]        @save_original[:kick_upgrade] = @save[:kick_upgrade]        @save_original[:punch_upgrade] = @save[:punch_upgrade]        File.open(#{$rpath}/require/player.yml, 'w') { |out|           YAML.dump(@save_original, out)        }    end    def save_game        File.open(#{$rpath}/require/player.yml, 'w') { |out|           YAML.dump(@save, out)        }    end    def save_score        score = @save[:zombies_killed]        scores = YAML.load_file(#{$rpath}/scores.yml)        if score > scores.last[0]            p_level_up High Score! What is your name?            name = prompt            scores = scores.push([score, name]).sort_by { |i| -i[0]}            scores.pop            File.open(#{$rpath}/scores.yml, w) { |out|                 YAML.dump(scores, out)            }        end    end    def take_damage damage        @save[:health] -= damage    end    def taunt        taunt = [HEY ZOMBIE! UR FACE!, DIRT BAG, UR MOM, POOP FACE, GET OWNED BUDDY BOY, :p, EAT MY FIST, be nice, You stink, YO MAMA].rand_choice        if @save[:xp] >= 2            xp = (-12..12).to_a.rand_choice            self.give_xp xp            p_pain #{taunt} #{(xp >= 0 ? + : -)}#{xp.abs} xp            @save[:taunts_available] -= 1        else            p_warn You are missing the necessary xp to taunt (2)        end    end    def upgrade        max_level = 6        if @save[:kick_upgrade] >= max_level && @save[:punch_upgrade] >= max_level            return        else            p_level_up What do you want to upgrade? (kick or punch)            skill = prompt            while !([kick, punch].include? skill)                p_warn Please answer with kick or punch. What would you like to upgrade?                skill = prompt            end            max_level_message = #{skill} is at the max level (6)            plus_1 = p_level_up #{skill} +1            case skill            when kick                if @save[:kick_upgrade] < max_level                    @save[:kick_upgrade] += 1                    p_level_up plus_1                else                     p_warn max_level_message                    upgrade                end            when punch                if @save[:punch_upgrade] < max_level                    @save[:punch_upgrade] += 1                    p_level_up plus_1                else                     p_warn max_level_message                    upgrade                end            end        end    endendrequire/zombie.rb:class Zombie     attr_accessor :is_alive, :name, :power, :xp, :phrases, :health    def initialize        @is_alive = true        self.setInfo    end    def setInfo        @name = Zombie        @power = (4..6)        @health = 10        @xp = 2        @phrases = [hit ur face, punched the heck out of you, beat the heck out of you, bruised ur face]    end    def info        _power = @power.to_a        p_info #{@name} health: #{@health}        p_info Attack Strength: #{_power[0]} to #{_power[-1]}    end    def attack        @power.to_a.rand_choice    end    def take_damage amount        @health -= amount    end    def check_dead        self.die if @health <= 0    end    def die        p_pain KO! You killed the #{@name}        @is_alive = false    endend# start extra zombiesclass BigZombie < Zombie    def setInfo        @name = Big Zombie        @power = (6..8)        @health = 15        @xp = 4        @phrases = [hit ur face, punched the heck out of you, beat the heck out of you, bruised ur face]    endendclass DaddyZombie < Zombie    def setInfo        @name = Daddy Zombie        @power = (4..10)        @health = 20        @xp = 9        @phrases = [IS your daddy, punched the heck out of you, beat the heck out of you, ain't your mom, told you to go to bed]    endendclass GunZombie < Zombie    def setInfo        @name = Gun Zombie        @power = (3..15)        @health = 20        @xp = 15                @phrases = [shot yo face, shot the heck out of you, beat the heck out of you, made you eat bullets, showed you his ak-47]    endendclass NinjaZombie < Zombie    def setInfo        @name = Ninja Zombie        @power = (7..20)        @health = 20        @xp = 18        @phrases = [was to ninja for you, threw a ninja star at your face, is a blur, sent you flying, has a black belt]    endendclass IdiotZombie < Zombie    def setInfo        @name = Idiot Zombie        @power = (7..20)        @health = 2        @xp = 5        @phrases = [is an idiot but still pwn-ed u, fell down from stupidness but somehow landed on you, beat ur face's face, is somehow beating you]    endendclass BlindZombie < Zombie    def setInfo        @name = Blind Zombie        @power = (1..25)        @health = 24        @xp = 20        @phrases = [tried to hit you, cant see ur face, cant touch this, cant see you but hurt you anyway]    endendclass StrongZombie < Zombie    def setInfo        @name = Strong Zombie        @power = (15..21)        @health = 30        @xp = 30        @phrases = [destroyed you, may have murdered you, is strong, is VERY strong, works out]    endendclass BasicallyDeadZombie < Zombie    def setInfo        @name = Basically Dead Zombie        @power = (50..75)        @health = (2..5).to_a.rand_choice        @xp = (2..5).to_a.rand_choice        @phrases = [totally pwn-ed you!, hurt you pretty bad, obliterated you, probably killed you, is not dead]    endendclass SuperZombie < Zombie    def setInfo        @name = Super Zombie        @power = (35..56)        @health = 65        @xp = 38        @phrases = [is up up and away!, just chucked kryptonite at ur face, has super strength, is the ultimate super villain, just mad you cry]    endendclass BossZombie < Zombie    def setInfo        @name = Boss Zombie        @power = (60..90)        @health = (95..105).to_a.rand_choice        @xp = (65..80).to_a.rand_choice        @phrases = [sent you to work!, is not giving you a raise, is a boss!, just fired you, just demoted you]    endendclass UltimateZombie < Zombie    def setInfo        @name = Ultimate Zombie        @power = (75..115)        @health = 100        @xp = 115        @phrases = [is to ultimate, is really scary, just gave you a nice punch to the face, is more ultimate than you, makes you look... un-ultimate]    endend
Zombie Smack Down game
beginner;ruby;game;meta programming
I would like to start by saying that I've never worked with ruby 1.8.7, so I apologize in advance if some of my observations are not relevant for that version...Require relativeYou use $rpath where you assume that your app is installed somewhere, which is a very big assumption. Don't do that.I know, require_relative is not supported on 1.8.7, but it appears to have a workaround. This way you can install your app anywhere, and it will still work.Global methodsYou use global methods to drive your printing. Global methods are 'magical', in the sense that a reader cannot be sure where they came from.A better solution might be to put these methods in a module, maybe call it ColoredLogger, and include it in classes you want to use it in.Patching classesYou are patching the String class. Patching a class should be done very rarely, as it may affect unaware third parties.Your use of patching seems hardly warranted, as you add for an internal single use in the same place you define it.Simply define the color table anywhere else:Colors = {black: { prefix: 30, suffix: 0 },red: { prefix: 31, suffix: 0 },green: { prefix: 32, suffix: 0 },brown: { prefix: 33, suffix: 0 },blue: { prefix: 34, suffix: 0 },magenta: { prefix: 35, suffix: 0 },cyan: { prefix: 36, suffix: 0 },gray: { prefix: 37, suffix: 0 },bg_black: { prefix: 40, suffix: 0 },bg_red: { prefix: 41, suffix: 0 },bg_green: { prefix: 42, suffix: 0 },bg_brown: { prefix: 43, suffix: 0 },bg_blue: { prefix: 44, suffix: 0 },bg_magenta: { prefix: 45, suffix: 0 },bg_cyan: { prefix: 46, suffix: 0 },bg_gray: { prefix: 47, suffix: 0 },bold: { prefix: 1, suffix: 22 },reverse_color: { prefix: 7, suffix: 27 }}def colorize(color, string)  \033[#{Colors[color][:prefix]}m#{string}\033[#{Colors[color][:suffix]}mendor even simply do it hard coded in your methods.Your Array patch seems much more relevant than the String patch.Variables vs. constantsYour CLI class's initializer populates combos and commands with hard coded data which never changes. They should be declared as constants:class CLI  COMMANDS = %w[ kick punch combo combolist taunt info scores quit help heal easter ]  COMBOS = { kick punch => KickPunch.new,    # ..  }What are classes for?Your Combo classes and Zombie classes are not really classes, but configuration - they do not add any functionality, and therefore should not be classes.You can use JSON configuration files, where you can keep the names and vitals of each flavor you need, and Combo and Zombie instances may simply refer to a flavor:{ KickPunch: { name: Kick Punch,    price: 2,    min_damage: 3,    max_damage: 9},  TripStomp: { ... },  ....}State once-removedYou save your player's state in a @save hash inside your instance. This is not very object oriented, and doesn't seem to be warranted - facilitating a quick save is not a good enough reason. When you save, simply collect the relevant data.It is also unclear what happens if the saved file does not exist when the game starts.What does reset do?Give clear names to your methods and members. What does @save_original hold? Why does reset change it? Why is it called when a player dies?You have two different methods called attack which seem to do completely different stuff - one is a getter, while the other manages the battle. You might want to reconsider their names.Model View ControllerYour design is not very MVC - the model prompts the user, and gives the user feedback, etc. Make an effort to separate the game logic from user interactions, it would make adapting your game to different UIs a lot easier, or should I say - possible.When to use *args?You use *args quite liberally in your Cli class. *args are used when you want to have a dynamic number of arguments. It is not a solution for a catch-all API. You want to tell the user of your API that he uses your API with too many arguments, rather than swallow it.Cli.combo some, [garbage, that], :should_not, Be, hereI don't think that it is in the scope of CR to hunt down camelCase methods in your code, so I'm not going to do it (setInfo)