Simple RPG - Alice fights Bob This is designed to be a simple example of a RPG battle, for a talk I hope to give. The rules:


Alice and Bob both have 5 hit points and inflict 2 damage.
Each turn, Alice attacks Bob, and Bob attacks Alice.
3/4 of attacks hit their target.
1/2 of the attacks that hit their target are blocked, inflicting half damage.
When one character is dead, the game is over.


Important ideas:


Sending the attack and receiving the attack are handled separately.
It's a re-usable API for characters, so game.rb is a single use case. There could be others.


character.rb

class Character

  attr_accessor :name, :hit_points

  def initialize(name, hit_points, hit_chance, block_chance, damage)
    @name = name
    @hit_points = hit_points
    @hit_chance = hit_chance
    @block_chance = block_chance
    @damage = damage
  end

  def attack(other_character)
    puts "#{name} attacks #{other_character.name}"
    if roll(@hit_chance)
      puts "  The attack hits"
      other_character.be_attacked(@damage)
    else
      puts "  The attack missed"
    end
  end

  def be_attacked(damage)
    if roll(@block_chance)
      puts "  It was blocked, half damage done."
      take_damage(damage / 2)
    else
      take_damage(damage)
    end
  end

  def take_damage(damage)
    @hit_points -= damage
    puts "  -#{damage} hit points"
    puts "  #{name} has been defeated" if dead?
  end

  def dead?
    @hit_points &lt;= 0
  end

  private

  def roll(chance)
    rand(100) &lt; chance
  end

end


game.rb

class Game

  def initialize
    @alice = Character.new('Alice', 5, 75, 50, 2)
    @bob = Character.new('Bob', 5, 75, 50, 2)
  end

  def turn
    @alice.attack(@bob)
    @bob.attack(@alice)
    report_status
    sleep 1
  end

  def over?
    (@alice.dead? or @bob.dead?)
  end

  def report_status
    puts ''
    if over?
      puts "The game is over!"
    else
      puts "Alice: #{@alice.hit_points} - Bob: #{@bob.hit_points}"
      puts ''
    end
  end

end


script.rb (executable file)

require_relative 'character.rb'
require_relative 'game.rb'

game = Game.new
game.turn until game.over?


Example output:



Alice attacks Bob
  The attack missed
Bob attacks Alice
  The attack hits
  -2 hit points

Alice: 3 - Bob: 5

Alice attacks Bob
  The attack hits
  -2 hit points
Bob attacks Alice
  The attack missed

Alice: 3 - Bob: 3

Alice attacks Bob
  The attack hits
  -2 hit points
Bob attacks Alice
  The attack hits
  -2 hit points

Alice: 1 - Bob: 1

Alice attacks Bob
  The attack hits
  It was blocked, half damage done.
  -1 hit points
  Bob has been defeated
Bob attacks Alice
  The attack hits
  -2 hit points
  Alice has been defeated

The game is over!
When one character is dead, the game is over.


No, it isn't, because in your own example output it says:


  Alice attacks Bob [...] Bob has been defeated
  
  Bob attacks Alice [...] Alice has been defeated
  
  The game is over!


Bob is apparently dangerous, even in death.

Anyway, code review:

I'd prefer that the characters weren't responsible for printing anything. Their responsibility should only be to be number generators; let the game handle the encounter.

I'd also advise against using attr_accessor :hit_points, since that means you can set the hit points from anywhere. Using attr_reader would be more fitting, I think.

Lastly, naming you instance variables @alice and @bob is rather too specific; they're both just characters. Besides, you might want more at some point.

Here's how I might do the character class:

class Character
  attr_reader :name, :hit_points, :damage

  def initialize(name, hit_points, damage, hit_chance, block_chance)
    @name = name
    @hit_points = hit_points
    @damage = damage
    @hit_chance = hit_chance
    @block_chance = block_chance
  end

  def dead?
    @hit_points &lt;= 0
  end

  def hits?
    roll(@hit_chance)
  end

  def blocks?
    roll(@block_chance)
  end

  def take_damage(amount)
    @hit_points -= amount
  end

  def block_damage
    @block_damage ||= (damage / 2).round
  end

  private

  def roll(threshold)
    rand(100) &gt; threshold
  end
end


As you can see, it just a data model; it doesn't manipulate anything outside itself.

For the game class, here's a simple implementation that has 3 characters in the mix (everyone gets an attack at each of the others, which isn't too RPG'ish, but it's just for illustration).

class Game
  def initialize
    @characters = []
    @characters &lt;&lt; Character.new("Alice", 5, 2, 75, 50)
    @characters &lt;&lt; Character.new("Bob", 5, 2, 75, 50)
    @characters &lt;&lt; Character.new("Carol", 5, 2, 75, 50)
  end

  def play
    while play_turn
      puts "----------------------------"
      print_status
      puts "----------------------------"
      sleep 1
    end
  end

  def play_turn
    living_characters.permutation(2) do |a, b|
      puts "#{a.name} attacks #{b.name}"

      if a.hits?
        blocked = b.blocks?
        damage = blocked ? a.block_damage : a.damage
        puts "\t#{b.name} blocks" if blocked
        puts "\t#{a.name} inflicts #{damage} points of damage"
        b.take_damage(damage)
        puts "\t#{b.name} has been defeated" if b.dead?
      else
        puts "\t#{a.name} misses"
      end

      if living_characters.size == 1
        puts "Game over, #{a.name} wins"
        return false
      end
    end
    true
  end

  def print_status
    living_characters.each do |player|
      puts "#{player.name}: #{player.hit_points} hit points"
    end
  end

  private

  def living_characters
    @characters.reject(&amp;:dead?)
  end
end


The game class is responsible for printing too, which isn't necessary the nicest way to do things. It conflates the view and controller, even though the model (the character) is now nicely encapsulated. However, I don't know the exact ruleset and UI you're going for, so I'll leave it as-is.

This class also does things a little differently than yours: The game ends the moment there's only one character left alive; recently dead characters do not get a final swing.