Note: This didn't seem to get through the first time I sent it. And now that I look at the online archives, I see my solution to last weeks isn't there either. Perhaps the list didn't like the fact that I attached zip files, which I did merely for the convenience of the person that puts the solutions on rubyquiz.com so they won't have to copy-paste. I'll resend my 119 solution in a bit, which was long and ugly, but did handle things like parentheses. --------------------------- Here's another long-winded solution from me. I'm not sure its correct, but for a few small examples (2 & 3 fingers per hand), it seems to know when a win is unavoidable given perfect play. There are 2 main classes that do the important work: State: This holds two Arrays, one for each player (@players). Each of those hold two numbers - how many fingers are on each hand. These are kept sorted, because it doesn't matter if you have 3-left and 2-right or 2-right and 3-left. State also has a @turn, which designates who moves next, a @parent, for back-tracing through a state tree, and a @best_outcome, which holds a value assigned by StateTree. Only @players and @turn are taken into account by ==, eql?, and hash. State's each_reachable_state method yields for each state reachable from itself, using the touch and clap rules. StateTree: This class holds a @root State, and a recursive best_outcome method. This method will be called for each reachable state from its argument. Whichever leads to the best outcome for that State's player (@turn) is returned. Then there's a few other classes and modules, like Outcome, which holds constants P1Win, P2Win, and Loop; and Game, which creates a StateTree, runs best_outcome, and prints the results. The number of fingers per hand can be changed from 5 in constants.rb. There are some commented-out simple examples in magic_fingers.rb, which seem to work: > Game.new([1,1], [0,4]).analyze Player 1, with perfect play, can win: 1: ----| |---- * 2: ----- ||||- 1: ----| |---- 2: ----- ----- * > Game.new([4,4], [1,1]).analyze Player 1, with perfect play, can win: 1: -|||| ||||- * 2: ----| |---- 1: -|||| ||||- 2: ----- |---- * 1: ----- ||||- * 2: ----- |---- 1: ----- ||||- 2: ----- ----- * > Game.new([0,1], [3,3]).analyze No matter how well Player 1 plays, Player 2 can win. For example: 1: ----- |---- * 2: --||| |||-- 1: ----- |---- 2: --||| ||||- * 1: ----- ----- * 2: --||| ||||- However, for the regular game: > Game.new.analyze Player 1 can stay in a loop. So my program thinks player 1 can forever repeat a loop of game states, but can't force a win. This contradicts my initial guess, but I don't know if its correct, so I think I have a bug. Here's the code: #!/usr/bin/env ruby # constants.rb Left, Right = 0, 1 # hand array indices Player1, Player2 = 0, 1 # player array indices FingersPerHand = 5 #!/usr/bin/env ruby # outcome.rb require 'constants' module Outcome # Order important: from best-for-player-1 to best-for-player-2 Outcome::P1Win = 0 Outcome::Loop = 1 Outcome::P2Win = 2 # Given an array of outcomes, return the one that is best for the given # player. def Outcome.best(player, outcomes) (player == Player1) ? outcomes.min : outcomes.max end end #!/usr/bin/env ruby # state.rb require 'constants' require 'set' # Represents one state of the game, which includes how many fingers are on each # player's hands, and whose turn it is. States can also have parents, and a # best_outcome can be assigned to them, though this class doesn't do anything # with that itself. The player's hands are always sorted, since it doesn't # matter having 3-left and 2-right is equivalent to 2-left and 3-right. When # comparing states with ==, eql?, or their hashes, only @players and @turn are # taken into account. class State attr_reader :players, :turn, :parent, :best_outcome attr_writer :best_outcome # player_1 and player_2 are Arrays of number-of-fingers on each hand. def initialize(player_1, player_2, turn, parent = nil) @players = [player_1, player_2] @turn = turn @parent = parent @best_outcome = nil for player in @players do State.normalize(player) end self end def hand_alive?(player_num, hand_num) @players[player_num][hand_num] > 0 end def player_alive?(player_num) hand_alive?(player_num, Left) or hand_alive?(player_num, Right) end # true if at least one player is dead. def end_state? not player_alive?(Player1) or not player_alive?(Player2) end # Return the winner. This should only be called on end states (otherwise, # it'll always return Player1). def winner player_alive?(Player1) ? Player1 : Player2 end # Turn the given player's hand into a fist if it has >= FingesPerHand # fingers up, and sort the hands. def State.normalize(player) for hand_num in [Left, Right] do player[hand_num] = 0 if player[hand_num] >= FingersPerHand end player.sort! end # Return a nice string representation of a player. def player_string(player_num) player = @players[player_num] '-' * (FingersPerHand-player[Left]) + '|' * player[Left] + ' ' + '|' * player[Right] + '-' * (FingersPerHand-player[Right]) end # Return a nice string representation of this state (including both player # strings). def to_s s = "1: #{player_string(Player1)}" s << ' *' if @turn == Player1 s << "\n2: #{player_string(Player2)}" s << ' *' if @turn == Player2 s end # Equality only tests the players' hands and the turn. def ==(other) @players == other.players and @turn == other.turn end # Both eql? and hash are defined so that Sets/Hashes of states will only # differentiate states based on @players and @turn. def eql?(other); self == other; end def hash; [@players, @turn].hash; end # Yield once for each ancestor state, starting from the oldest and ending on # this state. def each_ancestor ancestors = [self] while not ancestors.last.parent.nil? ancestors << ancestors.last.parent end ancestors.reverse_each { |a| yield a } end # Have one player (the toucher) touch the other player (the touchee). # Also, the touchee is then normalized. def State.touch(toucher, toucher_hand, touchee, touchee_hand) touchee[touchee_hand] += toucher[toucher_hand] State.normalize(touchee) end # Yield each state reachable from self. def each_reachable_state reachable = Set[] # use a Set instead of yielding as we find them to remove # yielding duplicates. player = @players[@turn] opponent_num = (@turn + 1) % 2 opponent = @players[opponent_num] # Touch rules. for player_hand in [Left, Right] do for opponent_hand in [Left, Right] do if hand_alive?(@turn, player_hand) and hand_alive?(opponent_num, opponent_hand) op = opponent.clone # because touch modifies it State.touch(player, player_hand, op, opponent_hand) if @turn == Player1 reachable << State.new(player, op, opponent_num, self) else reachable << State.new(op, player, opponent_num, self) end end end end # Clap rules. for source_hand in [Left, Right] do target_hand = (source_hand == Left ? Right : Left) # The first line is the number that can be removed from the source. # The second is the number that can be added to the target without killing # it. max_transfer = [player[source_hand] - 1, (FingersPerHand - player[target_hand] - 1)].min (1..max_transfer).each do |i| p = player.clone p[source_hand] -= i p[target_hand] += i State.normalize(p) if @turn == Player1 reachable << State.new(p, opponent.clone, opponent_num, self) else reachable << State.new(opponent.clone, p, opponent_num, self) end end end reachable.each { |r| yield r } end end #!/usr/bin/env ruby # state_tree.rb require 'constants' require 'state' require 'outcome' # Represents a tree of States in parent-child relationships. class StateTree attr_reader :root, :p1_win_node, :p2_win_node, :loop_node def initialize(root = State.new([1,1], [1,1], Player1)) @root = root @old_nodes = Set[@root] @p1_win_node = @p2_win_node = nil self end # Determine the best outcome at the given node, for that node's player. # # If Outcome::P1Win is returned, @p1_win_node will hold one way for player 1 # to win. Likewise for Outcome::P2Win. # # Because @old_nodes is filled up as it runs, it can only be called once. # Can't clear out @old_nodes at the end of the method, since its recursive, # and the next level up depends on it. Could just create a wrapper method # to do that, though. def best_outcome(node = @root) reachable_outcomes = [] node.each_reachable_state do |reached| if @old_nodes.include?(reached) if reached.best_outcome.nil? reachable_outcomes << Outcome::Loop else reachable_outcomes << reached.best_outcome end elsif reached.end_state? # Note that end states are never added to @old_nodes, because @old_nodes # is only used for loop-checking, and loops shouldn't include end # states. if reached.winner == Player1 @p1_win_node = reached reachable_outcomes << Outcome::P1Win else @p2_win_node = reached reachable_outcomes << Outcome::P2Win end else @old_nodes << node reachable_outcomes << best_outcome(reached) end end node.best_outcome = Outcome.best(node.turn, reachable_outcomes) end end #!/usr/bin/env ruby # game.rb require 'constants' require 'outcome' require 'state' require 'state_tree' class Game # Constructor. p1_start and p2_start are Arrays containing the number of # fingers on each of their hands at the start of the game. def initialize(p1_start = [1,1], p2_start = [1,1]) @state_tree = StateTree.new(State.new(p1_start, p2_start, Player1)) self end # Print out an analysis of the game. def analyze outcome = @state_tree.best_outcome if outcome == Outcome::P1Win puts 'Player 1, with perfect play, can win:' @state_tree.p1_win_node.each_ancestor do |state| puts "#{state}\n\n" end elsif outcome == Outcome::P2Win puts 'No matter how well Player 1 plays, Player 2 can win. For example:' @state_tree.p2_win_node.each_ancestor do |state| puts "#{state}\n\n" end elsif outcome == Outcome::Loop puts 'Player 1 can stay in a loop.' else puts 'I am broken.' end end end #!/usr/bin/env ruby # magic_fingers.rb # Ruby Quiz 120: Magic Fingers require 'game' Game.new.analyze # Normal, default game. #Game.new([1,1], [0,4]).analyze # P1 should win on move 1. #Game.new([4,4], [1,1]).analyze # P1 should win on move 2. #Game.new([0,1], [3,3]).analyze # P2 should win on move 2. -- Jesse Merriman jessemerriman / warpmail.net http://www.jessemerriman.com/