I've been having a lot of fun with this quiz. Pit Capitain sent me a
improved version of Amb that is more "Rubyish" and much easier to read
(my original was a direct translation of the scheme version). I've
added comments and a bit of polish to his cleanup, so here's the new and
improved version of Amb along with another puzzle (just for fun).
-- BEGIN AMB
---------------------------------------------------------------
#!/usr/bin/env ruby
# Copyright 2006 by Jim Weirich (jim / weirichhouse.org). All rights
reserved.
# Permission is granted for use, modification and distribution as
# long as the above copyright notice is included.
# Amb is an ambiguous choice maker. You can ask an Amb object to
# select from a discrete list of choices, and then specify a set of
# constraints on those choices. After the constraints have been
# specified, you are guaranteed that the choices made earlier by amb
# will obey the constraints.
#
# For example, consider the following code:
#
# amb = Amb.new
# x = amb.choose(1,2,3,4)
#
# At this point, amb may have chosen any of the four numbers (1
# through 4) to be assigned to x. But, now we can assert some
# conditions:
#
# amb.assert (x % 2) == 0
#
# This asserts that x must be even, so we know that the choice made by
# amb will be either 2 or 4. Next we assert:
#
# amb.assert x >= 3
#
# This further constrains our choice to 4.
#
# puts x # prints '4'
#
# Amb works by saving a contination at each choice point and
# backtracking to previousl choices if the contraints are not
# satisfied. In actual terms, the choice reconsidered and all the
# code following the choice is re-run after failed assertion.
#
# You can print out all the solutions by printing the solution and
# then explicitly failing to force another choice. For example:
#
# amb = Amb.new
# x = Amb.choose(*(1..10))
# y = Amb.choose(*(1..10))
# amb.assert x + y == 15
#
# puts "x = #{x}, y = #{y}"
#
# amb.failure
#
# The above code will print all the solutions to the equation x + y ==
# 15 where x and y are integers between 1 and 10.
#
# The Amb class has two convience functions, solve and solve_all for
# encapsulating the use of Amb.
#
# This example finds the first solution to a set of constraints:
#
# Amb.solve do |amb|
# x = amb.choose(1,2,3,4)
# amb.assert (x % 2) == 0
# puts x
# end
#
# This example finds all the solutions to a set of constraints:
#
# Amb.solve_all do |amb|
# x = amb.choose(1,2,3,4)
# amb.assert (x % 2) == 0
# puts x
# end
#
class Amb
class ExhaustedError < RuntimeError; end
# Initialize the ambiguity chooser.
def initialize
@back = [
lambda { fail ExhaustedError, "amb tree exhausted" }
]
end
# Make a choice amoung a set of discrete values.
def choose(*choices)
choices.each { |choice|
callcc { |fk|
@back << fk
return choice
}
}
failure
end
# Unconditional failure of a constraint, causing the last choice to
# be retried. This is equivalent to saying
# <code>assert(false)</tt>.
def failure
@back.pop.call
end
# Assert the given condition is true. If the condition is false,
# cause a failure and retry the last choice.
def assert(cond)
failure unless cond
end
# Report the given failure message. This is called by solve in the
# event that no solutions are found, and by +solve_all+ when no more
# solutions are to be found. Report will simply display the message
# to standard output, but you may override this method in a derived
# class if you need different behavior.
def report(failure_message)
puts failure_message
end
# Class convenience method to search for the first solution to the
# constraints.
def Amb.solve(failure_message="No Solution")
amb = self.new
yield(amb)
rescue Amb::ExhaustedError => ex
amb.report(failure_message)
end
# Class convenience method to search for all the solutions to the
# constraints.
def Amb.solve_all(failure_message="No More Solutions")
amb = self.new
yield(amb)
amb.failure
rescue Amb::ExhaustedError => ex
amb.report(failure_message)
end
end
-- END AMB
---------------------------------------------------------------
And a new puzzle to go along with the new version of Amb:
-- BEGIN PUZZLE
-----------------------------------------------------------
#!/usr/bin/env ruby
# Two thieves have being working together for years. Nobody knows
# their identities, but one is known to be a Liar and the other a
# Knave. The local sheriff gets a tip that the bandits are about to
# commit another crime. When the sheriff arrives at the seen of the
# crime, he finds three men, A, B, and C. C has been stabbed with a
# dagger. He cries out, "A stabbed me" before anybody can say anything
# else; then, he falls down dead from the stabbing.
#
# Not sure which of the three men are the crooks, the sheriff takes
# the two suspects to the jail and interrogates them. He gets the
# following information.
#
# A's statements:
# 1. B is one of the crooks.
# 2. B?s second statement is true.
# 3. C was telling the truth.
#
# B's statements:
# 1. A killed the other guy.
# 2. C was killed by one of the thieves.
# 3. C?s next statement would have been a lie.
#
# C's statement:
# 1. A stabbed me.
#
# The sheriff knows that the murderer is among these three people. Who
# should the sheriff arrest for killing C?
#
# NOTE: Liars always lie, knights always tell the truth and knaves
# strictly alternate between truth and lies.
require 'amb'
# Some helper methods for logic
class Object
def implies(bool)
self ? bool : true
end
def xor(bool)
self ? !bool : bool
end
end
# True if the given list of boolean values alternate between true and
# false.
def alternating(*bools)
expected = bools.first
bools.each do |item|
if item != expected
return false
end
expected = !expected
end
true
end
# A person class to keep track of the information about a single
# person in our puzzle.
class Person
attr_reader :name, :type, :murderer, :thief
attr_accessor :statements
def initialize(amb, name)
@name = name
@type = amb.choose(:liar, :knave, :knight)
@murderer = amb.choose(true, false)
@thief = amb.choose(true, false)
@statements = []
end
def to_s
"#{name} is a #{type} " +
(thief ? "and a thief." : "but not a thief.") +
(murderer ? " He is also the murderer." : "")
end
end
# Some lists used to do collective assertions.
people = Array.new(3)
thieves = Array.new(2)
# Find all the solutions.
Amb.solve_all do |amb|
count = 0
# Create the three people in our puzzle.
a = Person.new(amb, "A")
b = Person.new(amb, "B")
c = Person.new(amb, "C")
people = [a, b, c]
# Basic assertions about the thieves.
thieves = people.select { |p| p.thief }
amb.assert thieves.size == 2 # Only two thieves
amb.assert thieves.collect { |p| # One is a knave, the other a liar
p.type.to_s
}.sort == ["knave", "liar"]
# Basic assertions about the murderer.
amb.assert people.select { |p| # There is only one murderer
p.murderer
}.size == 1
murderer = people.find { |p| p.murderer }
amb.assert ! c.murderer # No suicides
# Create the logic statements of each of the people involved. Note
# we are just creating them here. We won't assert the truth of them
# until a bit later.
c1 = a.murderer # A stabbed me
c2 = case c.type # (hypothetical next statement)
when :knight
false
when :liar
true
when :knave
!c1
end
c.statements = [c1, c2]
b1 = a.murderer # A killed the other guy
b2 = murderer.thief # C was killed by one of the thieves
b3 = ! c2 # C's next statement would have been
true
b.statements = [b1, b2, b3]
a1 = b.thief # B is one of the crooks
a2 = b2 # B's second statement is true
a3 = c1 # C was telling the truth.
a.statements = [a1, a2, a3]
# Now we make assertions on the truthfulness of each of persons
# statements based on whether they are a Knight, a Knave or a Liar.
people.each do |p|
amb.assert(
(p.type == :knight).implies(
p.statements.all? { |s| s }
))
amb.assert(
(p.type == :liar).implies(
p.statements.all? { |s| !s }
))
amb.assert(
(p.type == :knave).implies(
alternating(*p.statements)
))
end
# Now we print out the solution.
count += 1
puts "Solution #{count}:"
people.each do |p| puts p end
puts
end
-- END PUZZLE
-------------------------------------------------------------
-- Jim Weirich
--
Posted via http://www.ruby-forum.com/.