coding-horror/basic-computer-games · error · CardError
invalid suit
Error message
invalid suit
What it means
CardError is a custom Ruby exception (subclass of StandardError) defined inside War::Card. It is raised in Card#initialize when the suit argument is not one of :spades, :hearts, :clubs, or :diamonds (checked against the frozen SUITS constant). In the current codebase this guard is effectively dead code: the only call site is Card.shuffle (war.rb:40), which destructures pairs from CARDS = SUITS.product(PIPS), guaranteeing every suit is a valid SUITS member. CardError is never rescued anywhere, so if a bad suit were ever passed the program would crash with an unhandled exception traceback.
Source
Thrown at 94_War/ruby/war.rb:17
#!/usr/bin/env ruby
# reinterpreted from BASIC by stephan.com
class War
class Card
class CardError < StandardError; end
SUITS = %i[spades hearts clubs diamonds].freeze
PIPS = %i[ace deuce trey four five six seven eight nine ten jack king queen].freeze
CARDS = SUITS.product(PIPS).freeze
VALUES = PIPS.zip(1..13).to_h.freeze
attr_reader :value
def initialize(suit, pip)
@suit = suit
@pip = pip
raise CardError, 'invalid suit' unless SUITS.include? @suit
raise CardError, 'invalid pip' unless PIPS.include? @pip
@value = VALUES[pip]
end
def <=>(other)
@value <=> other.value
end
def >(other)
@value > other.value
end
def <(other)
@value < other.value
end
def to_sView on GitHub (pinned to 5301155192)
Solutions
- Validate the suit against SUITS before calling Card.new: raise CardError early with a clear message at the data boundary.
- Convert string input to symbol with .to_sym and check membership: SUITS.include?(suit.to_sym) before construction.
- Rescue War::Card::CardError at the call site to degrade gracefully instead of crashing.
- Always use Card.shuffle for standard decks, which provably never triggers the guard.
- Add a factory method Card.from_h or Card.from_json that validates and converts input before calling new.
Example fix
# before
card = Card.new('spades', :ace) # string, not symbol -> raises CardError
# after — validate and convert at the boundary
suit = suit.to_sym if suit.is_a?(String)
raise CardError, "invalid suit: #{suit}" unless SUITS.include?(suit)
card = Card.new(suit, pip)
# or rescue at call site
begin
card = Card.new(suit_input, pip_input)
rescue War::Card::CardError => e
warn "Skipping invalid card: #{e.message}"
next
end Defensive patterns
Strategy: validation
Validate before calling
# Validate suit symbol before constructing a Card
suit = suit.to_sym if suit.is_a?(String)
unless War::Card::SUITS.include?(suit)
warn "Ignoring invalid suit: #{suit.inspect}"
next # or raise, or use a default
end
card = War::Card.new(suit, pip) Type guard
# Type guard: narrow any input to a valid suit symbol or reject
module War
class Card
def self.valid_suit?(s)
SUITS.include?(s.to_sym)
end
def self.from_input(suit_str, pip_str)
suit = suit_str.to_sym
pip = pip_str.to_sym
return nil unless SUITS.include?(suit) && PIPS.include?(pip)
new(suit, pip)
end
end
end Try / catch
# Rescue CardError at the call site for graceful degradation
begin
card = War::Card.new(parsed_suit, parsed_pip)
rescue War::Card::CardError => e
warn "Invalid card data (#{e.message}), skipping."
next
end Prevention
- Always use Card.shuffle for standard 52-card decks — it provably never passes an invalid suit.
- When accepting suits from external sources (JSON, user input, network), call .to_sym and check SUITS.include? before Card.new.
- Define a factory method (Card.from_hash, Card.from_json) that validates at the deserialization boundary.
- Add CardError to a rescue clause at the top-level entry point so uncaught bad data logs instead of crashing.
- Keep SUITS and PIPS frozen constants — they are the single source of truth for valid values.
- Write a unit test that feeds every invalid suit type (string, nil, integer, wrong symbol) and asserts CardError is raised.
When it happens
Trigger: A future refactor or external caller passing Card.new(:joker, :ace), Card.new('spades', :ace) (string instead of symbol), or any suit not in the SUITS array. Deserializing saved game state from JSON/YAML where suits are strings. Parsing card names from user input or the original BASIC data format without converting to valid symbols.
Common situations: Adding custom deck support (jokers, extra suits). Serializing/deserializing card state across a network boundary. Internationalizing suit names. Writing test fixtures with typos in suit symbols. Refactoring PIPS/SUITS constants without updating all references.
Related errors
AI-assisted analysis of coding-horror/basic-computer-games@5301155192 (2026-08-13).
Data as JSON: /api/errors/9ce1feedf90ed60b.
Report an issue: GitHub.