Skip to content

Instantly share code, notes, and snippets.

@StandardGiraffe
Last active January 9, 2023 16:47
Show Gist options
  • Save StandardGiraffe/b946bc787def30807fe2f68de846d0c3 to your computer and use it in GitHub Desktop.
Save StandardGiraffe/b946bc787def30807fe2f68de846d0c3 to your computer and use it in GitHub Desktop.
class CondimentJar
class ContainerClosedError < StandardError; end
class ContainerEmptyError < StandardError; end
attr_accessor :contents
def initialize(contents = nil)
@contents = contents # Jar is empty by default.
@state_of_the_lid = :closed # I wasn't raised in a barn.
end
def empty?
contents.nil?
end
# All jars contain one portion for simplicity.
def has_stuff?
!empty?
end
def closed?
@state_of_the_lid == :closed
end
def close!
@state_of_the_lid = :closed
end
def open?
@state_of_the_lid == :open
end
def open!
@state_of_the_lid = :open
end
def relinquish_condiment!
if closed?
raise ContainerClosedError, "The jar is closed and knife-impermeable."
end
if empty?
raise ContainerEmptyError, "The jar is empty. How disappointing."
end
# Provide the contents and empty the container
return contents.tap { |t| self.contents = nil }
end
end
class Knife
class KnifeDirtyError < StandardError; end
attr_accessor :contents
def initialize
@contents = nil
end
def clean?
contents.nil?
end
def clean!
contents = nil
end
def loaded?
!clean?
end
def load_from!(container)
if loaded?
raise KnifeDirtyError, "This knife is already loaded. Don't mix your condiments!"
end
@contents = container.relinquish_condiment!
end
end
class SliceOfBread
class Surface
attr_accessor :contents
def initialize
@contents = nil
end
def plain?
contents.nil?
end
def smeared?
!plain?
end
end
class SurfaceAlreadySmearedError < StandardError; end
class KnifeNotLoadedError < StandardError; end
class InvalidKnifeError < StandardError; end
class InvalidSurfaceError < StandardError; end
attr_reader :top
attr_reader :bottom
def initialize
@top = Surface.new
@bottom = Surface.new
end
def plain?
top.plain? and bottom.plain?
end
def smeared?
top.smeared? or bottom.smeared?
end
def smear!(knife:, surface:)
unless knife.is_a?(Knife)
raise InvalidKnifeError, "That's not hygienic."
end
unless [:top, :bottom].include?(surface)
raise InvalidSurfaceError, "What're you, crazy? Put that knife away."
end
actual_surface = case surface
when :top
top
when :bottom
bottom
end
unless actual_surface.plain?
raise SurfaceAlreadySmearedError, "This surface was already smeared!"
end
unless knife.loaded?
raise KnifeNotLoadedError, "This knife is too clean to smear with."
end
actual_surface.contents = knife.contents
knife.contents = nil
end
end
class Sandwich
class NotEnoughSlicesError < StandardError; end
class OutsideSmearedError < StandardError; end
class AlreadyBuiltError < StandardError; end
class TooPlainError < StandardError; end
class ImmatureSandwichError < StandardError; end
class AlreadyCutError < StandardError; end
# These errors already exist in the Knife and SliceOfBread namespaces respectively; I feel okay about adding them again here because I might want to add specific code to these versions in the future.
class InvalidKnifeError < StandardError; end
class DirtyKnifeError < StandardError; end
attr_reader :slices
def initialize(*slices_of_bread)
@slices = slices_of_bread || [ ]
@built = false
@cut = false
end
def flavours
slices.flat_map do |slice|
[ slice.bottom.contents, slice.top.contents ]
end.uniq.compact
end
def flavours_human_readable
f = flavours.map(&:downcase)
if f.count == 2
return f.join(' and ')
end
f[-1] = 'and ' + f[-1]
f.join(', ')
end
def <<(slice)
@slices << slice
end
def ready_to_eat?
@built and @cut
end
def build!
if @built
raise AlreadyBuiltError, "It's already a glorious tower of food!"
end
if slices.count < 2
raise NotEnoughSlicesError, "#{slices.length} slices of bread does not a sandwich make."
end
unless slices.first.bottom.plain? and slices.last.top.plain?
raise OutsideSmearedError, "This sandwich would be icky to hold."
end
# Check all but the top slice for plainness
if slices[..-2].map(&:plain?).any?(true)
raise TooPlainError, "This sandwich might actually be a loaf."
end
@built = true
end
def cut!(knife)
unless @built
raise ImmatureSandwichError, "Build the sandwich and then cut it in one glorious stroke."
end
unless knife.is_a?(Knife)
raise InvalidKnifeError, "That's not hygienic."
end
unless knife.clean?
raise DirtyKnifeError, "No! You'll get the edge all yucky with that knife."
end
if @cut
raise AlreadyCutError, "One cut will do."
end
@cut = true
end
end
class Chef
class TakenForGrantedError < StandardError; end
class UnspeakablyBlandError < StandardError; end
def initialize(condiments: [], knife: nil, slices_of_bread: [])
@condiments = condiments
@knife = knife
@slices_of_bread = slices_of_bread
@under_appreciated = true
end
def please # Courtesy is important
@under_appreciated = false
self
end
def make_me_a_sandwich!(*requested_flavours)
# === Gate Conditions ====================================================
if @under_appreciated # Courtesy is important!
raise TakenForGrantedError, "Poof: you're a sandwich."
end
@under_appreciated = true # You get ONE request per courtesy.
# No vacuum- or integer-sandwiches please; they give me a headache.
requested_flavours.keep_if { |flavour| flavour.is_a? String }
if requested_flavours.count < 1
raise UnspeakablyBlandError, "I think you might just want some dry toast."
end
# Let's make sure we've got what we need and fix any problems.
ensure_condiments_are_actually_condiments
ensure_bread_is_actually_bread
ensure_knife
ensure_condiments_are_available(requested_flavours)
ensure_enough_bread_is_available(requested_flavours)
# Create an empty proto-sandwich. Really, just the idea of a sandwich.
sandwich = Sandwich.new
@knife.clean!
surface_to_smear = :top
current_slice = @slices_of_bread.pop
requested_flavours.each do |flavour|
condiment = @condiments.find { |jar| jar.contents == flavour}
condiment.open!
@knife.load_from!(condiment)
current_slice.smear!(
knife: @knife,
surface: surface_to_smear
)
# If ready, add the current slice to the stack and grab another.
if current_slice.top.smeared?
sandwich << current_slice
current_slice = @slices_of_bread.pop
end
# Swap the target surface for the next condiment.
surface_to_smear = (surface_to_smear == :top ? :bottom : :top)
end
# Add the final slice
sandwich << current_slice
# The magic happens
sandwich.build!
sandwich.cut!(@knife)
if sandwich.ready_to_eat? # How could it not be? Still.
puts "Finit. One #{sandwich.flavours_human_readable} sandwich; bon appétit!"
return sandwich # Needlessly explicit "return"; assume to be flourish.
else
puts "Hmm. Something went wrong despite my genius."
end
end
private
# Throw out any empty bottles and/or cans of paint.
def ensure_condiments_are_actually_condiments
@condiments.keep_if do |condiment|
condiment.is_a?(CondimentJar) and condiment.has_stuff?
end
end
# Gather any missing condiments...
def ensure_condiments_are_available(requested_flavours)
missing_condiments = [ ]
# Let's avoid multiple trips to the store by taking stock before we go.
requested_flavours.each do |flavour|
unless @condiments.find { |jar| jar.contents == flavour}
missing_condiments << flavour
end
end
if missing_condiments.any?
puts "Looks like we could use a few things from the store..."
missing_condiments.each do |condiment|
@condiments << CondimentJar.new(condiment)
puts "Bought me some #{condiment}"
end
puts "There we go. That's better."
end
end
# Safety first.
def ensure_knife
return if @knife.is_a?(Knife)
puts "I'll probably want a knife for this..."
@knife = Knife.new
puts "... Full tang mokume gane butter knife. Perfect."
end
# Keep our existing bread collection sane and unsullied.
def ensure_bread_is_actually_bread
@slices_of_bread.keep_if do |slice|
slice.is_a?(SliceOfBread) and slice.plain?
end
end
# Make sure we won't run out in the middle of the operation.
def ensure_enough_bread_is_available(requested_flavours)
sufficient_slices = 1 + (requested_flavours.count / 2.0).ceil
return if @slices_of_bread.count >= sufficient_slices
puts "This isn't enough bread to make your sandwich. Let me just grab some more."
while @slices_of_bread.count < sufficient_slices
@slices_of_bread << SliceOfBread.new
puts "Yoink!"
end
puts "... There we go. Pile o' bread!"
end
end
daddy = Chef.new
lunch = daddy.please.make_me_a_sandwich!("Relish", "Marmite", "Nutella™", "Sriracha")
pp lunch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment