Ruby Adventure
A single, self-running Ruby program covering 18 core language concepts — from variables to a working Mini RPG. Each section is annotated with explanations, tips, and expected output.
ruby ruby_adventure.rb
Variables & Data Types
Everything in Ruby is an object — even nil.Ruby is dynamically typed — you never declare a type. Variables are created the moment you assign a value. Every value, including nil and false, is a full object with its own methods.
# Ruby is dynamically typed — no type declarations needed. # Variables are created the moment you assign them. integer_val = 42 float_val = 3.14159 string_val = "Hello, Ruby!" boolean_true = true boolean_false = false nil_val = nil # nil is Ruby's "nothing" — it IS an object symbol_val = :my_symbol # Symbols: immutable, memory-efficient identifiers puts "Integer : #{integer_val} (class: #{integer_val.class})" puts "Float : #{float_val} (class: #{float_val.class})" puts "String : #{string_val} (class: #{string_val.class})" puts "Boolean : #{boolean_true} / #{boolean_false}" puts "Nil : #{nil_val.inspect} (class: #{nil_val.class})" puts "Symbol : #{symbol_val} (class: #{symbol_val.class})" # ── IMPORTANT: Ruby's Truthiness Rule ────────────────────────── # ONLY false and nil are falsy. 0, "", [] — all TRUTHY! (unlike Python/JS) [0, "", [], false, nil, "hello"].each do |val| puts " #{val.inspect.ljust(10)} is #{val ? 'TRUTHY' : 'FALSY'}" end
false and nil are falsy. That means if 0 will execute its body, unlike Python where it wouldn't.
Integer : 42 (class: Integer) Float : 3.14159 (class: Float) String : Hello, Ruby! (class: String) Boolean : true / false Nil : nil (class: NilClass) Symbol : my_symbol (class: Symbol) 0 is TRUTHY "" is TRUTHY [] is TRUTHY false is FALSY nil is FALSY "hello" is TRUTHY
Strings
Interpolation, heredocs, and a rich method library.Double-quoted strings support interpolation (#{expr}) and escape sequences (\n, \t). Single-quoted strings are completely literal — no interpolation, no escape processing. Use <<~HEREDOC for clean multi-line strings.
name = "Ruby" year = 1995 # String interpolation: only works in double-quoted strings puts "#{name} was created in #{year}." # interpolation ✓ puts '#{name} — single quotes are LITERAL' # no interpolation ✗ puts "Math inside interpolation: #{2 ** 10}" # => 1024 # Common string methods sentence = " the quick brown fox " puts sentence.strip # remove whitespace puts sentence.strip.capitalize # first letter uppercase puts sentence.strip.upcase # ALL CAPS puts sentence.strip.split(" ").inspect # split into Array puts "hello".center(20, "-") # ------- hello ------- puts "ha" * 5 # hahahahaha puts "hello world".gsub("o", "0") # replace all puts "hello world".include?("fox") # => false # Multiline string (heredoc) — the ~ strips leading whitespace poem = <<~HEREDOC Roses are red, Violets are blue, Ruby is awesome, And so are you. HEREDOC puts poem # String formatting printf "%-15s %5d\n", "Count:", 42 # left-align, right-align puts "Pi is approximately %.4f" % Math::PI # 4 decimal places
.strip · .chomp · .upcase · .downcase · .capitalize · .split(delim) · .join(glue) · .gsub(from, to) · .include?(str) · .start_with? · .end_with? · .chars · .length · .reverse · .to_i · .to_f
Numbers & Math
Integer vs Float division is the #1 gotcha.puts 10 / 3 # => 3 (INTEGER division — truncates!) puts 10.0 / 3 # => 3.333... (Float division) puts 10 % 3 # => 1 (modulo / remainder) puts 2 ** 8 # => 256 (exponent — NOT ^ like other languages) puts -7.abs # => 7 puts 3.7.ceil # => 4 (round up) puts 3.7.floor # => 3 (round down) puts 3.567.round(2) # => 3.57 # Underscores in large numbers (readability only) puts 1_000_000 # => 1000000 puts 255.to_s(16) # => "ff" (to hex string) puts 0xFF # => 255 (hex literal) puts 42.between?(1, 100) # => true # Random numbers puts rand(10) # random Integer 0..9 puts rand(1.0..2.0).round(3) # random Float in range puts [*1..10].sample # random element from range-array
10 / 3 returns 3, not 3.333. To get float division, make at least one operand a Float: 10.0 / 3 or 10.to_f / 3. This catches everyone at least once.
Symbols
Immutable, interned identifiers — not strings.A Symbol is written with a leading colon: :name. The same symbol always resolves to the exact same object in memory — unlike strings, which create a new object each time. This makes symbols perfect for hash keys, method names, and status flags.
a = :status b = :status puts "Same object? #{a.object_id == b.object_id}" # => true ✓ # Strings with same content are DIFFERENT objects puts "status".object_id == "status".object_id # => false ✗ # Common uses: hash keys, method names, status flags status = :active puts status # active puts status.to_s # "active" puts "active".to_sym # :active puts :hello.upcase # :HELLO # Symbols as status flags in a hash order = { id: 1001, status: :pending, priority: :high } case order[:status] when :pending then puts "Order is pending" when :shipped then puts "Order shipped!" when :delivered then puts "Delivered" end
Arrays
Ordered, flexible, mixed-type collections with a powerful API.fruits = ["apple", "banana", "cherry", "date", "elderberry"] # ── Accessing ─────────────────────────────────────────────────── puts fruits[0] # "apple" (first element) puts fruits[-1] # "elderberry" (last — negative index!) puts fruits[1, 3].inspect # start index=1, take 3 items puts fruits[0..2].inspect # range slice puts fruits.first(2).inspect puts fruits.last(2).inspect # ── Modifying ─────────────────────────────────────────────────── fruits.push("fig") # add to end fruits << "grape" # << is the append "shovel" operator fruits.unshift("avocado") # add to front popped = fruits.pop # remove & return last element shifted = fruits.shift # remove & return first element # ── Transformation (non-destructive — returns new array) ──────── nums = [4, 8, 15, 16, 23, 42] puts nums.map { |n| n * 2 }.inspect # transform each element puts nums.select { |n| n.even? }.inspect # keep matching elements puts nums.reject { |n| n.even? }.inspect # keep NON-matching puts nums.sort_by { |n| -n }.inspect # sort descending # ── Aggregating ───────────────────────────────────────────────── puts nums.sum # => 108 puts nums.reduce(:+) # same — symbol shorthand puts nums.reduce(1, :*) # product of all puts nums.count { |n| n.even? } # count matching a condition puts nums.any? { |n| n > 40 } # => true puts nums.all? { |n| n > 0 } # => true puts nums.none? { |n| n > 100 } # => true # ── Flatten, Compact & Set Operations ─────────────────────────── puts [1, [2, 3], [4, [5]]].flatten.inspect # [1,2,3,4,5] puts [1, nil, 2, nil, 3].compact.inspect # remove nils puts ([1,2,3] & [2,3,4]).inspect # intersection [2,3] puts ([1,2] | [2,3]).inspect # union [1,2,3] puts ([1,2,3] - [2]).inspect # difference [1,3]
! return a new array: nums.sort leaves nums unchanged. Methods with ! mutate the original: nums.sort! modifies nums directly. Same rule applies to map!, select!, reverse!, etc.
Hashes
Key-value maps — the workhorse of Ruby data structures.# Modern style uses symbol keys (key: value) ─────────────────── person = { name: "Alice", age: 30, city: "Atlanta", hobbies: ["reading", "hiking", "ruby"] } # Old "hash rocket" style: any type can be a key old_style = { "name" => "Alice", 42 => "the answer" } # ── Accessing ─────────────────────────────────────────────────── puts person[:name] # => "Alice" puts person[:missing].inspect # => nil (no error!) puts person.fetch(:age) # => 30 puts person.fetch(:missing, "N/A") # => "N/A" (safe default) # ── Modifying ─────────────────────────────────────────────────── person[:email] = "alice@example.com" # add or update person.delete(:city) # remove a key # ── Iterating ─────────────────────────────────────────────────── person.each { |key, val| puts " #{key}: #{val}" } # ── Useful methods ────────────────────────────────────────────── puts person.keys.inspect puts person.values.inspect puts person.key?(:name) puts person.value?(31) # ── Merge & Transform ─────────────────────────────────────────── defaults = { role: "user", active: true, score: 0 } settings = { score: 100, theme: "dark" } merged = defaults.merge(settings) # second hash wins on conflicts puts merged.inspect scores = { alice: 95, bob: 72, carol: 88, dave: 55 } passing = scores.select { |_, score| score >= 70 } puts passing.inspect doubled = scores.transform_values { |v| v * 2 } puts doubled.inspect
Ranges
Inclusive (..) vs exclusive (...) — and they work on strings too.inclusive = (1..10) # 1 to 10 (includes 10) exclusive = (1...10) # 1 to 9 (excludes 10) letters = ('a'..'f') # works on strings! puts inclusive.to_a.inspect # [1,2,3,4,5,6,7,8,9,10] puts exclusive.to_a.inspect # [1,2,3,4,5,6,7,8,9] puts letters.to_a.inspect # ["a","b","c","d","e","f"] puts inclusive.include?(10) # => true puts exclusive.include?(10) # => false puts inclusive.sum # => 55 # step — iterate with custom increment print "step by 5: " (0..20).step(5) { |n| print "#{n} " } puts # Ranges as case/when conditions (uses === internally) [5, 13, 25, 67].each do |age| category = case age when 0..12 then "Child" when 13..17 then "Teenager" when 18..64 then "Adult" else "Senior" end puts " #{age} => #{category}" end
Control Flow
if, unless, ternary, case/when — Ruby's logic tools.temperature = 72 # Standard if/elsif/else if temperature > 90 puts "Hot day!" elsif temperature > 70 puts "Nice day — #{temperature}°F" else puts "Cool day" end # unless = "if not" — reads naturally unless temperature < 60 puts "No jacket needed" end # Postfix (one-liner) — very idiomatic Ruby! puts "Perfect temperature!" if temperature.between?(68, 76) puts "Bring a coat!" unless temperature > 60 # Ternary operator label = temperature > 80 ? "Warm" : "Comfortable" puts "It's: #{label}" # case/when — uses === internally so ranges, regex, classes all work grade_score = 87 grade = case grade_score when 90..100 then "A" when 80..89 then "B" when 70..79 then "C" when 60..69 then "D" else "F" end puts "Score #{grade_score} => Grade #{grade}" # case with no argument acts as if/elsif chain x = 42 case when x < 0 then puts "Negative" when x == 0 then puts "Zero" when x < 100 then puts "#{x} is a smallish positive number" else puts "Large" end
return if condition and raise unless valid are deeply idiomatic Ruby. They read like English and make guard clauses at the top of methods incredibly clean. Embrace them early.
Loops & Iteration
Ruby prefers .each and iterators over traditional for loops.# times — simplest counted loop (block variable is 0-based) 5.times { |i| print "#{i} " } # 0 1 2 3 4 # upto / downto 1.upto(5) { |i| print "#{i} " } # 1 2 3 4 5 5.downto(1) { |i| print "#{i} " } # 5 4 3 2 1 # step (0..20).step(5) { |n| print "#{n} " } # 0 5 10 15 20 # while i = 1 while i <= 5 print "#{i} " i += 1 end # until = "while not" i = 5 until i == 0 print "#{i} " i -= 1 end # loop — explicit infinite loop with break count = 0 loop do print "#{count} " count += 1 break if count >= 5 end # each — most common Ruby loop [10, 20, 30, 40, 50].each { |n| print "#{n} " } # each_with_index — when you need the index ["alpha", "beta", "gamma"].each_with_index do |word, idx| puts " [#{idx}] #{word}" end # next (like continue) and break (1..10).each do |n| next if n.even? # skip even numbers break if n > 7 # stop when n exceeds 7 print "#{n} " end # prints: 1 3 5 7
Methods
Implicit return, keyword arguments, splat — Ruby methods are flexible.# Last expression is automatically returned — no return keyword needed def square(n) n ** 2 end puts square(7) # => 49 # Default parameters def greet(name, greeting = "Hello", punctuation = "!") "#{greeting}, #{name}#{punctuation}" end puts greet("Alice") # => "Hello, Alice!" puts greet("Bob", "Hi") # => "Hi, Bob!" # Keyword arguments (named, order-independent, self-documenting) def create_profile(name:, age:, city: "Unknown", active: true) "#{name}, #{age}, #{city} [active: #{active}]" end puts create_profile(name: "Dave", age: 28, city: "Denver") puts create_profile(age: 22, name: "Eve") # order doesn't matter! # Splat *args — captures extra positional args as an Array def sum_all(*numbers) numbers.reduce(0, :+) end puts sum_all(1, 2, 3, 4, 5) # => 15 # Double splat **kwargs — captures extra keyword args as a Hash def log_event(event, **metadata) puts "EVENT: #{event}" metadata.each { |k, v| puts " #{k}: #{v}" } end log_event("login", user: "alice", ip: "127.0.0.1", success: true) # ? suffix — predicate methods (returns boolean by convention) def palindrome?(str) str == str.reverse end puts palindrome?("racecar") # => true # Multiple return values via Array (destructure on the caller's side) def min_max(arr) [arr.min, arr.max] end low, high = min_max([4, 1, 9, 2, 7]) puts "min=#{low}, max=#{high}"
Blocks, Procs & Lambdas
Ruby's most distinctive feature — code you pass around like data.A block is an anonymous chunk of code attached to a method call. Procs are saved blocks. Lambdas are stricter procs (check argument count, have their own return scope). The &:method_name trick converts a symbol to a proc — one of Ruby's most-used idioms.
# yield — calls the block passed to a method def measure(label) start = Time.now result = yield # executes the caller's block elapsed = Time.now - start puts "#{label}: #{result.inspect} (#{elapsed.round(6)}s)" end measure("square of 1000") { 1000 ** 2 } # block_given? — check if a block was passed def optional_block(x) block_given? ? yield(x) : x * 2 end puts optional_block(5) # => 10 puts optional_block(5) { |n| n + 100 } # => 105 # Proc — a saved, reusable block double = Proc.new { |n| n * 2 } triple = proc { |n| n * 3 } puts double.call(7) # => 14 puts triple.(4) # .() is shorthand for .call() puts double[9] # [] also calls a Proc # Lambda — stricter Proc (checks arg count, return stays in lambda) square_it = lambda { |n| n ** 2 } add_one = ->(n) { n + 1 } # stabby lambda syntax (preferred) puts square_it.call(6) # => 36 puts add_one.(9) # => 10 # &:symbol — converts a symbol to a block (very idiomatic!) words = ["hello", "WORLD", "Ruby"] puts words.map(&:downcase).inspect # same as .map { |w| w.downcase } puts words.map(&:length).inspect # [5, 5, 4] # Method chaining — a functional pipeline result = (1..20) .select(&:odd?) # keep odd numbers .map { |n| n ** 2 } # square them .reject { |n| n > 100 } # drop those over 100 .reduce(:+) # sum puts "Pipeline result: #{result}" # => 1+9+25+49+81 = 165
["a","b"].map(&:upcase) is equivalent to ["a","b"].map { |s| s.upcase }.
The & converts :upcase into a block. This works with any method name as a symbol, and is used constantly throughout Ruby codebases.
Classes & OOP
attr_accessor, initialize, class methods, to_s — all the essentials.class BankAccount INTEREST_RATE = 0.035 # Constant — never changes # attr_accessor :x creates def x; @x; end AND def x=(v); @x=v; end # attr_reader :x creates getter only (read-only from outside) attr_reader :owner, :account_number attr_accessor :balance @@total_accounts = 0 # Class variable — shared across ALL instances def initialize(owner, initial_balance = 0) @owner = owner @balance = initial_balance.to_f @@total_accounts += 1 @account_number = "ACC-%04d" % @@total_accounts @transactions = [] end def deposit(amount) raise ArgumentError, "Deposit must be positive" unless amount > 0 @balance += amount @transactions << { type: :deposit, amount: amount } puts " Deposited $#{"%.2f" % amount} → Balance: $#{"%.2f" % @balance}" end def withdraw(amount) raise ArgumentError, "Must be positive" unless amount > 0 raise "Insufficient funds" if amount > @balance @balance -= amount @transactions << { type: :withdrawal, amount: amount } puts " Withdrew $#{"%.2f" % amount} → Balance: $#{"%.2f" % @balance}" end def apply_interest deposit(@balance * INTEREST_RATE) end def overdrawn? = @balance < 0 # one-line method (Ruby 3+) def self.total_accounts # Class method — called on BankAccount @@total_accounts end def to_s # called when object is printed "#{@account_number} [#{@owner}] $#{"%.2f" % @balance}" end end alice_acc = BankAccount.new("Alice", 1000) puts alice_acc alice_acc.deposit(250) alice_acc.withdraw(150) alice_acc.apply_interest puts "Total accounts: #{BankAccount.total_accounts}"
Inheritance
Single inheritance with super, polymorphism, and is_a?class Animal attr_accessor :name, :age def initialize(name, age) @name = name @age = age end def speak = "..." # base — to be overridden by subclasses def describe "#{@name} (#{@age} yrs) says: #{speak}" end end class Dog < Animal # < means "inherits from" attr_accessor :breed def initialize(name, age, breed) super(name, age) # calls Animal#initialize @breed = breed end def speak = "Woof! Woof!" # override the parent method def fetch(item) = "#{@name} fetches the #{item}!" end class Cat < Animal def initialize(name, age, indoor: true) super(name, age) @indoor = indoor end def speak = "Meow~" def indoor? = @indoor end class Parrot < Animal def initialize(name, age, phrase) super(name, age) @phrase = phrase end def speak = "#{@phrase}! Squawk!" end animals = [ Dog.new("Rex", 3, "Labrador"), Cat.new("Whiskers", 5), Parrot.new("Polly", 7, "Pretty bird") ] # Polymorphism — each object responds to .describe differently animals.each { |a| puts a.describe } puts animals[0].is_a?(Animal) # => true (Dog IS AN Animal) puts animals[0].is_a?(Cat) # => false puts animals[0].class # => Dog
Modules & Mixins
Share behavior across classes without multiple inheritance.Ruby has single inheritance (one parent class), but you can include as many modules as you want. This is the primary way to share behavior across unrelated classes. Modules also serve as namespaces — use Module::ClassName to prevent naming collisions.
module Describable def full_description vars = instance_variables.map do |var| "#{var}=#{instance_variable_get(var).inspect}" end "#{self.class.name}(#{vars.join(', ')})" end end module Taggable def tag_list @tags ||= [] # ||= : assign only if @tags is nil or false end def add_tag(tag) tag_list << tag.to_sym unless tag_list.include?(tag.to_sym) end def tagged_with?(tag) = tag_list.include?(tag.to_sym) end module Serializable def to_csv instance_variables.map { |v| instance_variable_get(v) }.join(",") end end class Product include Describable # adds instance methods from module include Taggable include Serializable attr_accessor :name, :price, :category def initialize(name, price, category) @name = name; @price = price; @category = category end end laptop = Product.new("ThinkPad X1", 1299.99, :electronics) laptop.add_tag("portable") laptop.add_tag("business") puts laptop.full_description puts laptop.to_csv puts laptop.tagged_with?(:business) # => true puts laptop.tagged_with?(:gaming) # => false
Comparable Mixin
Define <=> once and get sort, min, max, and comparison operators free.class Student include Comparable # gives <, >, <=, >=, ==, between?, sort attr_accessor :name, :gpa def initialize(name, gpa) @name = name @gpa = gpa end # Define the spaceship operator — Comparable does the rest! # Returns: -1 (less), 0 (equal), 1 (greater) def <=>(other) @gpa <=> other.gpa end def to_s = "#{@name}(#{@gpa})" end students = [ Student.new("Alice", 3.9), Student.new("Bob", 3.1), Student.new("Carol", 3.7), Student.new("Dave", 2.8) ] puts students.sort.map(&:to_s).inspect puts "Top: #{students.max}" puts "Lowest: #{students.min}" honor_roll = students.select { |s| s.gpa >= 3.5 } puts "Honor roll: #{honor_roll.map(&:name).inspect}" puts students[0] > students[1] # => true (Alice GPA 3.9 > Bob GPA 3.1)
Exception Handling
begin/rescue/else/ensure/retry — and custom exception classes.# Custom exception class — inherit from StandardError class InsufficientFundsError < StandardError def initialize(needed, available) super("Need $#{needed}, but only $#{available} available") end end # Full begin/rescue/else/ensure structure def safe_divide(a, b) begin result = a / b rescue ZeroDivisionError => e puts " ✗ ZeroDivision: #{e.message}" result = nil rescue TypeError => e puts " ✗ TypeError: #{e.message}" result = nil else puts " ✓ #{a} / #{b} = #{result}" # only if NO exception raised ensure puts " (ensure always runs — like finally)" end result end safe_divide(10, 2) safe_divide(10, 0) # retry — reattempt the block on failure attempts = 0 begin attempts += 1 raise "Flaky error" if attempts < 3 puts "Succeeded after #{attempts} attempt(s)" rescue RuntimeError => e puts " Attempt #{attempts} failed. Retrying..." retry if attempts < 3 end # raise with custom exception + guard clauses def transfer(amount, from_balance) raise ArgumentError, "Amount must be positive" if amount <= 0 raise InsufficientFundsError.new(amount, from_balance) if amount > from_balance from_balance - amount end [[-50, 500], [200, 100], [100, 500]].each do |amount, balance| begin result = transfer(amount, balance) puts " ✓ Transferred $#{amount}. New balance: $#{result}" rescue ArgumentError, InsufficientFundsError => e puts " ✗ #{e.class}: #{e.message}" end end
| Clause | When it runs | Equivalent in other languages |
|---|---|---|
rescue | When the specified error is raised | catch |
else | Only if NO exception was raised | No direct equivalent |
ensure | Always — exception or not | finally |
retry | Inside rescue — jumps back to begin | No direct equivalent |
File I/O
Read, write, append — File.open blocks auto-close the file handle.filename = "ruby_output.txt" # Writing — block form auto-closes the file File.open(filename, "w") do |f| # "w" = write (overwrites) f.puts "Ruby Adventure Output" f.puts "Generated at: #{Time.now}" f.puts "─" * 30 end # file is automatically closed here # Appending — "a" mode adds to end of file File.open(filename, "a") do |f| # "a" = append f.puts "Another line added later" end # Reading entire file at once content = File.read(filename) puts content # Reading line by line (memory efficient for large files) line_count = 0 File.foreach(filename) { |_| line_count += 1 } puts "Lines: #{line_count}" # Metadata puts File.size(filename) # bytes puts File.exist?(filename) # true/false # User input from terminal (interactive programs) # print "Enter your name: " # name = gets.chomp # gets reads a line, chomp removes the trailing \n # Cleanup File.delete(filename) puts "File deleted." # Directory operations puts Dir.exist?(".") # true puts Dir.glob("*.rb").inspect # list all .rb files
🎮 Mini Text RPG — Putting It All Together
All 17 concepts wired into a working, self-running program.module Combatant def alive? = @hp > 0 def dead? = !alive? def hp_bar filled = (@hp.to_f / @max_hp * 20).round "[" + "█" * filled + "░" * (20 - filled) + "] #{@hp}/#{@max_hp}" end end class Hero include Combatant attr_reader :name, :hp, :level, :gold, :inventory LEVEL_XP = [0, 100, 250, 450, 700] # XP threshold per level def initialize(name) @name = name @level = 1 @hp = @max_hp = 30 @attack = 8 @xp = 0 @gold = 10 @inventory = [] end def attack_roll base = rand(@attack..@attack * 2) # Range for random roll bonus = @inventory.include?(:sword) ? 5 : 0 base + bonus end def take_damage(dmg) = @hp = [@hp - dmg, 0].max # clamp to 0 def heal(amount) = @hp = [@hp + amount, @max_hp].min def gain_xp(xp) @xp += xp threshold = LEVEL_XP[@level] || 999 if @xp >= threshold && @level < 4 @level += 1 @max_hp += 10 @hp = @max_hp @attack += 3 puts " ⬆ #{@name} reached Level #{@level}! HP ↑ ATK ↑" end end def to_s "#{@name} Lv#{@level} #{hp_bar} ATK:#{@attack} GOLD:#{@gold}g" end end
class Monster include Combatant attr_reader :name, :xp_reward, :gold_reward # Symbols as keys in a nested Hash of monster stats TYPES = { slime: { hp: 8, atk: 2..4, xp: 30, gold: 2..5 }, goblin: { hp: 15, atk: 3..7, xp: 60, gold: 5..10 }, orc: { hp: 25, atk: 5..10, xp: 120, gold: 8..15 }, dragon: { hp: 50, atk: 8..18, xp: 300, gold: 20..40 } } def initialize(type) data = TYPES[type] @name = type.to_s.capitalize @hp = @max_hp = data[:hp] @atk_range = data[:atk] @xp_reward = data[:xp] @gold_reward = rand(data[:gold]) end def attack_roll = rand(@atk_range) def to_s = "#{@name} #{hp_bar}" end # battle method — uses blocks, symbols, conditional logic def battle(hero, monster) puts " ⚔ #{hero.name} VS #{monster.name}!" round = 0 while hero.alive? && monster.alive? round += 1 dmg_to_monster = hero.attack_roll monster.take_damage(dmg_to_monster) dmg_to_hero = monster.alive? ? monster.attack_roll : 0 hero.take_damage(dmg_to_hero) puts " Round #{round}: Hero hits #{dmg_to_monster} / #{monster.name} hits #{dmg_to_hero}" break unless monster.alive? end if hero.alive? hero.gain_xp(monster.xp_reward) puts " ✓ #{hero.name} wins! +#{monster.xp_reward}XP +#{monster.gold_reward}G" else puts " ✗ #{hero.name} was defeated..." end puts " Hero: #{hero}" hero.alive? end # ── Run the demo adventure ──────────────────────────────────────── hero = Hero.new("Rubyist") puts "#{hero.name} begins their adventure!" puts hero [:slime, :slime, :goblin, :orc].each do |type| puts monster = Monster.new(type) break unless battle(hero, monster) hero.heal(5) end puts "\nAdventure complete!" puts hero
Rubyist begins their adventure! Rubyist Lv1 [████████████████████] 30/30 ATK:8 GOLD:10g ⚔ Rubyist VS Slime! Round 1: Hero hits 14 / Slime hits 3 ✓ Rubyist wins! +30XP +3G Hero: Rubyist Lv1 [████████████████░░░░] 27/30 ATK:8 GOLD:10g ⚔ Rubyist VS Slime! Round 1: Hero hits 9 / Slime hits 4 ✓ Rubyist wins! +30XP +4G ⬆ Rubyist reached Level 2! HP ↑ ATK ↑ Hero: Rubyist Lv2 [████████████████████] 40/40 ATK:11 GOLD:10g ⚔ Rubyist VS Goblin! Round 1: Hero hits 16 / Goblin hits 5 Round 2: Hero hits 12 / Goblin hits 0 ✓ Rubyist wins! +60XP +7G Hero: Rubyist Lv2 [█████████████████░░░] 35/40 ATK:11 GOLD:10g Adventure complete! Rubyist Lv2 [█████████████████░░░] 35/40 ATK:11 GOLD:10g