// A Complete Beginner's Program

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.

18 Sections ~450 lines of Ruby Zero Dependencies Run: ruby ruby_adventure.rb
§ 01

Variables & Data Types

Everything in Ruby is an object — even nil.
IntegerFloat StringBoolean NilClassSymbol Dynamic TypingTruthiness

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_adventure.rb — Section 1
# 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
⚡ Coming from Python or JavaScript?
0 and "" are TRUTHY in Ruby. This trips up almost every newcomer. Only false and nil are falsy. That means if 0 will execute its body, unlike Python where it wouldn't.
▶ Expected Output
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
§ 02

Strings

Interpolation, heredocs, and a rich method library.
InterpolationSingle vs Double Quotes Heredocstrip/upcase/split gsub/include?printf formatting

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.

ruby_adventure.rb — Section 2
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
✦ Key Methods Quick Reference
.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
§ 03

Numbers & Math

Integer vs Float division is the #1 gotcha.
Integer / FloatArithmetic Operators ceil/floor/round/absrandUnderscores
ruby_adventure.rb — Section 3
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
⚠ Integer Division Trap
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.
§ 04

Symbols

Immutable, interned identifiers — not strings.
:symbol syntaxImmutability object_idHash keysState flags

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.

ruby_adventure.rb — Section 4
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
§ 05

Arrays

Ordered, flexible, mixed-type collections with a powerful API.
push/pop/shift/unshiftmap/select/reject reduce/sum/min/maxflatten/compact/uniq Negative indexingSet operations
ruby_adventure.rb — Section 5
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]
ℹ Bang Methods (!) — In-place vs Copy
Methods without ! 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.
§ 06

Hashes

Key-value maps — the workhorse of Ruby data structures.
Symbol keysHash rocket => fetch with defaultmerge transform_valuesselect on Hash
ruby_adventure.rb — Section 6
# 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
§ 07

Ranges

Inclusive (..) vs exclusive (...) — and they work on strings too.
.. inclusive... exclusive stepinclude?to_acase/when
ruby_adventure.rb — Section 7
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
§ 08

Control Flow

if, unless, ternary, case/when — Ruby's logic tools.
if/elsif/elseunless Postfix if/unlessTernary ?: case/whenRange in case
ruby_adventure.rb — Section 8
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
✦ Postfix Style — The Ruby Way
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.
§ 09

Loops & Iteration

Ruby prefers .each and iterators over traditional for loops.
times/upto/downtowhile/until loop doeacheach_with_index next/break/redo
ruby_adventure.rb — Section 9
# 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
§ 10

Methods

Implicit return, keyword arguments, splat — Ruby methods are flexible.
def/endImplicit return Default argsKeyword args *splat**double splat ? and ! naming
ruby_adventure.rb — Section 10
# 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}"
§ 11

Blocks, Procs & Lambdas

Ruby's most distinctive feature — code you pass around like data.
{ } blockdo..end block yieldblock_given? Proc.new-> lambda &:symbol shorthandChaining

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.

ruby_adventure.rb — Section 11
# 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
💎 Most Idiomatic Ruby Pattern
["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.
§ 12

Classes & OOP

attr_accessor, initialize, class methods, to_s — all the essentials.
class/endinitialize @instance vars@@class vars attr_accessor/reader/writer self.class_methodto_s CONSTANTSprivate
ruby_adventure.rb — Section 12
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}"
§ 13

Inheritance

Single inheritance with super, polymorphism, and is_a?
class Child < Parentsuper Method overridingPolymorphism is_a? / class
ruby_adventure.rb — Section 13
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
§ 14

Modules & Mixins

Share behavior across classes without multiple inheritance.
module/endinclude extend||= lazy init Namespace::

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.

ruby_adventure.rb — Section 14
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
§ 15

Comparable Mixin

Define <=> once and get sort, min, max, and comparison operators free.
include ComparableSpaceship <=> .sort .min .maxbetween? clamp
ruby_adventure.rb — Section 15
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)
§ 16

Exception Handling

begin/rescue/else/ensure/retry — and custom exception classes.
begin/rescue/ensureraise retryelse clause Custom exceptionsMultiple rescue
ruby_adventure.rb — Section 16
# 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
ClauseWhen it runsEquivalent in other languages
rescueWhen the specified error is raisedcatch
elseOnly if NO exception was raisedNo direct equivalent
ensureAlways — exception or notfinally
retryInside rescue — jumps back to beginNo direct equivalent
§ 17

File I/O

Read, write, append — File.open blocks auto-close the file handle.
File.openFile.read File.foreachFile.write gets.chompAppend mode "a"
ruby_adventure.rb — Section 17
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
§ 18

🎮 Mini Text RPG — Putting It All Together

All 17 concepts wired into a working, self-running program.
💎 What This Section Demonstrates
Modules as mixins (Combatant) · Classes & inheritance (Hero, Monster) · Symbols as hash keys (TYPES hash) · Constants (LEVEL_XP) · @@class vars · Ranges for random rolls · Blocks, iterators, reduce · Guard clauses · each_with_index · Multiple assignment (h, m = divmod) · Ternary operators · All running together in a mini game loop.
All previous concepts combined Module mixinClass constants Symbol hash keysrand with Range Self-running demo
ruby_adventure.rb — Section 18 (Part A: Module & Hero)
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
ruby_adventure.rb — Section 18 (Part B: Monster & Game Loop)
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
▶ Sample Output
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