-
-
Save StandardGiraffe/b946bc787def30807fe2f68de846d0c3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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