# ------------------------------------------------------------------------------ # pegboard.rb # ------------------------------------------------------------------------------ # == Copyright # # Copyright (c) 2012 thundernet development group, inc. All rights reserved. # # == Synopsis # # Provides solutions for triangular 15 peg puzzle game commonly found at Cracker # Barrel restaurants. Also provides data for self-analysis of the algorithms # used to generate the solutions. Perhaps this can also be used as "inspiration" # for an Android version. # # == Usage # # $ ruby pegboard.rb [-h | -? | --help] # # ------------------------------------------------------------------------------ # # Given a peg board with 15 holes and 14 pegs, jump pegs to empty spots until # only 1 remains. A typical starting board (and solution) will look like this: # # o o 0 # x x o o 1 2 # x x x = = => o o o 3 4 5 # x x x x o o o o 6 7 8 9 # x x x x x o o x o o 10 11 12 13 14 # # First I'll need a Peg (a class to represent a position on the board). A Peg # will maintain its state (open/occupied) and a list of adjacent Pegs over which # it COULD jump. # # Then I'll need a Board. This will be a static, predefined, collection of Pegs # that know their place i.e. have built-in rules about who is adjacent to them. # All of this can be represented in a single multi-dimensional array. Like # 10-pin bowling, Peg 0 is at the top of the pyramid. # # For the moment, I'll assume the above starting board ... # # ------------------------------------------------------------------------------ require 'pp' require 'tdglib' #----- predefined CONSTANT values ----- # OPEN = false # indicates that a given board position is open or un-occupied OCCUPIED = true # indicates that a given board position is occupied # # each element of START represents an initial position on a game board. Each # element is an array where the first element represents the state of a given # position (open or occupied), and the 2nd element is an array of neighbors. # Each element of a neighbor array is a 2-element array indicating the source # and destination positions one can jump through and to from the given position. # START = [ [1, [[1,3], [2,5]]], # Peg 0 can jump over Pegs 1 (to 3) and 2 (to 5) [1, [[3,6], [4,8]]], # Peg 1 can jump over Pegs 3 (to 6) and 4 (to 8) [1, [[4,7], [5,9]]], # Peg 2 can jump over Pegs 4 (to 7) and 5 (to 9) [1, [[1,0], [4,5], [6,10], [7,12]]], # Peg 3 can jump over Pegs 1 (to 0), 4 (to 5), 6 (to 10), and 7 (to 12) [1, [[7,11], [8,13]]], # Peg 4 can jump over Pegs 7 (to 11) and 8 (to 13) [1, [[2,0], [4,3], [8,12], [9,14]]], # Peg 5 can jump over Pegs 2, 4, 8, and 9 [1, [[3,1], [7,8]]], # Peg 6 can jump over Pegs 3 and 7 [1, [[4,2], [8,9]]], # Peg 7 can jump over Pegs 4 and 8 [1, [[4,1], [7,6]]], # Peg 8 can jump over Pegs 4 and 7 [1, [[5,2], [8,7]]], # Peg 9 can jump over Pegs 5 and 8 [1, [[6,3], [11,12]]], # Peg 10 can jump over Pegs 6 and 11 [1, [[7,4], [12,13]]], # Peg 11 can jump over Pegs 7 and 12 [0, [[7,3], [8,5], [11,10], [13,14]]], # Peg 12 can jump over Pegs 7, 8, 11, and 13 [1, [[8,4], [12,11]]], # Peg 13 can jump over Pegs 8 and 12 [1, [[9,5], [13,12]]], # Peg 14 can jump over Pegs 9 and 13 ] class BadMoveError < RuntimeError # nothing special ... just a name to use end class Peg #------------------------------------------------------------------------------- # public attributes #------------------------------------------------------------------------------- attr_reader :state, :neighbors #------------------------------------------------------------------------------- # Peg::initialize() #++ # constructor - defines and initializes object attributes #------------------------------------------------------------------------------- def initialize(state = OPEN, neighbors = Array.new) @state = state.true? if neighbors.kind_of? Array @neighbors = neighbors else @neighbors = Array.new end end #------------------------------------------------------------------------------- # Peg::jumped!() #++ # Tells this Peg that it has been jumped and must OPEN its state. Raises an # exception if it is already OPEN. #------------------------------------------------------------------------------- def jumped! if @state == OPEN raise BadMoveError, "you can't jump over me, I'm already OPEN!" end @state = OPEN end #------------------------------------------------------------------------------- # Peg::jumped_neighbor() #++ # Who do I have to jump over to get to where you're sending me? #------------------------------------------------------------------------------- def jumped_neighbor(dst) j = nil @neighbors.each do | neighbor | j = neighbor[0] if neighbor[1] == dst end if j.nil? raise BadMoveError, "You can't get there from here" end return j end #------------------------------------------------------------------------------- # Peg::move_in! #++ # Tells this position that there's a new peg in town! #------------------------------------------------------------------------------- def move_in! if @state == OCCUPIED raise BadMoveError, "You can't come in here ... this space is occupied!" end @state = OCCUPIED end #------------------------------------------------------------------------------- # Peg::move_out! #++ # Peg (like Elvis) has left the building #------------------------------------------------------------------------------- def move_out! if @state == OPEN raise BadMoveError, "You must be lost. No one here but us mice!" end @state = OPEN end #------------------------------------------------------------------------------- # Peg::occupied?() #++ # indicates whether we are an OCCUPIED peg, or not. #------------------------------------------------------------------------------- def occupied? return @state == OCCUPIED end #------------------------------------------------------------------------------- # Peg::to_s() #++ # default string representation of the Peg #------------------------------------------------------------------------------- def to_s @state == OPEN ? 'o' : 'x' end end # end of class Peg class Board #------------------------------------------------------------------------------- # public attributes # # @solutions collection of puzzle solutions - each element is a series # of moves taken to solve it #------------------------------------------------------------------------------- attr_writer :solutions #------------------------------------------------------------------------------- # private attributes # # @pegs array of Peg positions on the board - one for each hole # @dupes counter of duplicate (non-unique) solutions found #------------------------------------------------------------------------------- #------------------------------------------------------------------------------- # Board::initialize() #++ # constructor - defines and initializes object attributes #------------------------------------------------------------------------------- def initialize(pegs = Array.new) # for this application, the given pegs array SHOULD have 15 elements. # Any missing elements will get default (empty) positions. Excess # positions will be ignored. n = min(pegs.length, 15) i = 0 @pegs = Array.new # use the elements in the given pegs array n.times do @pegs[i] = Peg.new(pegs[i][0], pegs[i][1]) i += 1 end # now deal with any shortage in the given pegs array (15 - i).times do @pegs[i] = Peg.new(0) i += 1 end # ... and, of course, initially our solutions are blank/unkonwn @solutions = Array.new @dupes = 0 end #------------------------------------------------------------------------------- # Board::analyze() #++ # returns a hash containing 'interesting' summary values about our solutions #------------------------------------------------------------------------------- def analyze h = Hash.new(0) # set zero as a default value f = File.open("pegboard.dat", "w") # how many solutions do we have? h[:solutions] = @solutions.length # which is the last peg standing? h[:last_peg] = Hash.new(0) @solutions.each do | solution | f << "#{mkhash(solution)} :: #{solution}\n" final_step = solution[-1] last_peg = final_step[:choices][final_step[:choice]][1] h[:last_peg][last_peg] += 1 end f.close return h end #------------------------------------------------------------------------------- # Board::choices() #++ # returns an array containing all the moves possible with the current pegs #------------------------------------------------------------------------------- def choices choices = Array.new i_choice = 0 i_peg = 0 # iterate our Pegs, looking for any that are OCCUPIED and have OCCUPIED # neighbors they can jump over to an OPEN target @pegs.each do | peg | if peg.occupied? peg.neighbors.each do | neighbor | jumpee = neighbor[0] target = neighbor[1] if (@pegs[jumpee].occupied? && !@pegs[target].occupied?) choices[i_choice] = [i_peg, target] i_choice += 1 end end end i_peg += 1 end return choices end #------------------------------------------------------------------------------- # Board::finished?() #++ # indicates whether any more moves can be made on the board. #------------------------------------------------------------------------------- def finished? return (choices().length == 0) end #------------------------------------------------------------------------------- # Board::mkhash() #++ # returns a 'hash' string for the given solution by simply concatenating the # numerals in the choice for each step therein #------------------------------------------------------------------------------- def mkhash(solution) hash = "" solution.each do | step | hash += step[:choice].to_s end return hash end #------------------------------------------------------------------------------- # Board::move() #++ # Attempts to move from the given source location to the destination. May # raise exceptions if you try to execute an invalid move. #------------------------------------------------------------------------------- def move(src, dst) if (!src.kind_of?(Fixnum) || !dst.kind_of?(Fixnum)) raise BadMoveError, "I can't work with this. Give me numbers people!" end jumpee = @pegs[src].jumped_neighbor(dst) if (!@pegs[src].occupied? or !@pegs[jumpee].occupied? or @pegs[dst].occupied?) raise BadMoveError, "That's just not right." end # OK, it's all good ... let's get a move on! @pegs[src].move_out! @pegs[jumpee].jumped! @pegs[dst].move_in! end #------------------------------------------------------------------------------- # Board::pegs_remaining() #++ # returns the number of Pegs remaining on our board #------------------------------------------------------------------------------- def pegs_remaining n = 0 @pegs.each do | peg | n += 1 if peg.occupied? end return n end #------------------------------------------------------------------------------- # Board::solutions= #++ # Records the given solution in our solutions collectin IF it is proven to be # a solution we haven't seen before ... and we have a VERY good memory! #------------------------------------------------------------------------------- def solutions=(candidate) # # before accepting this "new" solution ... let's make sure it really # IS unique. Remember that a solution is an array of steps and each # step contains a selected choice and a list of choices from which # to choose # unique = false unique = true if @solutions.length == 0 @solutions.each do | solution | # compare each solution with the candidate step by step istep = 0 solution.each do | step | same_choice = (step[:choice] == candidate[istep][:choice]) if !same_choice unique = true break end istep += 1 end # I know this process seems somewhat disjointed and laborious, but # this should execute quite a bit faster, especially as the list # of solutions grows if (!unique) # if at first we don't 'look' unique ... check the choices arrays istep = 0 solution.each do | step | same_choices = (step[:choices] == candidate[istep][:choices]) if !same_choices unique = true break end istep += 1 end end end if unique or true # ruby's .clone() and .dup() only do shallow copies i.e. the steps # in given solution will remain vulnerable to being overwritten # unless we do a deep copy here. The "cheap" method for doing that # is to marshal the candidate object out/in before storing @solutions << Marshal.load(Marshal.dump(candidate)) printf "found another solution: %d dupes: [%d]\r", @solutions.length, @dupes else @dupes += 1 end end #------------------------------------------------------------------------------- # Board::to_s() #++ # default string representation of the board #------------------------------------------------------------------------------- def to_s s = "\n" s << " #{@pegs[0].to_s}\n" s << " #{@pegs[1].to_s} #{@pegs[2].to_s}\n" s << " #{@pegs[3].to_s} #{@pegs[4].to_s} #{@pegs[5].to_s}\n" s << " #{@pegs[6].to_s} #{@pegs[7].to_s} #{@pegs[8].to_s} #{@pegs[9].to_s}\n" s << " #{@pegs[10].to_s} #{@pegs[11].to_s} #{@pegs[12].to_s} #{@pegs[13].to_s} #{@pegs[14].to_s}\n" return s end #------------------------------------------------------------------------------- # Board::unmove() #++ # Reverses a previous move denoted by the given original source and destination. # We will raise exceptions if you try to execute an invalid unmove. #------------------------------------------------------------------------------- def unmove(src, dst) if (!src.kind_of?(Fixnum) || !dst.kind_of?(Fixnum)) raise BadMoveError, "I can't with this. Give me numbers people!" end jumpee = @pegs[src].jumped_neighbor(dst) # ensure we've got pegs and openings in the right places ... if (@pegs[src].occupied? or @pegs[jumpee].occupied? or !@pegs[dst].occupied?) raise BadMoveError, "That's just not right." end # OK, it's all good ... let's get an unmove on! @pegs[src].move_in! @pegs[jumpee].move_in! @pegs[dst].move_out! end end # end of class Board #------------------------------------------------------------------------------- # solve by alan partis #++ # OK, now we need to get serious ... a 'solution' is composed of a number of # steps (a maximum of 13 of them if only 1 peg remains at the end). So we # could maintain an array of Steps that are composed of the available choices # at that time, plus the choice currently taken. At the end, the solution # should be obvious. # # We'll go through an iterative 'stack' approach to do this ... for the moment # anyway. This feels wrong -- it seems like I should be implementing a # recursive solution -- but almost "looks" right in my head. Let's see how it # works out on paper #------------------------------------------------------------------------------- def solve(board) done = false new_step = true solution = Array.new step = 0 # let's take the first step and see how it goes ... while (!done) if new_step # as we go up, initialize a new step solution[step] = Hash.new solution[step][:choice] = 0 solution[step][:choices] = board.choices end # make your move choice = solution[step][:choice] move_src = solution[step][:choices][choice][0] move_dst = solution[step][:choices][choice][1] board.move(move_src, move_dst) # if we're not finished then go to the next step if !board.finished? new_step = true step += 1 else # if we found a solution, report it. if (board.pegs_remaining == 1) board.solutions = solution end # whether we found a solution or not, we've come to the end of this # permutation. Undo the last move and see if there were other # choices we could make board.unmove(move_src, move_dst) # if there are more choices in this step, choose the next choice, but # stay in this step last_option = solution[step][:choices].length - 1 if (choice < last_option) # we have at least one more choice in this step ... solution[step][:choice] += 1 new_step = false else # # out of choices in this step ... look back for a step with # choices remaining # while ((step > 0) and (choice == last_option)) solution.pop step -= 1 choice = solution[step][:choice] move_src = solution[step][:choices][choice][0] move_dst = solution[step][:choices][choice][1] board.unmove(move_src, move_dst) last_option = solution[step][:choices].length - 1 # if there are more choices in this previous step, choose the # next one and again, stay in that step if (choice < last_option) solution[step][:choice] += 1 new_step = false end end if ((step == 0) and (choice == last_option)) # there's nothing left to try -- we're done done = true end end end end end #------------------------------------------------------------------------------- # main program entry point by alan partis #++ # Initialize our options and proceed to find a solution for our game board. # Print out completion stats when done. #------------------------------------------------------------------------------- begin board = Board.new(START) solve board puts board if board.finished? puts "#{board.pegs_remaining} pegs remain on the board" case board.pegs_remaining when 0 then puts "something's wrong ... the board is empty!" when 1 then puts "You're a genius." when 2 then puts "You're purty smart." when 3 then puts "You're just plain dumb." else puts "You're eg-no-ra-moose!" end else puts "You're not done yet, there are still more moves available." end puts "analyzing ... results in pegboard.dat when I'm done." pp board.analyze rescue StandardError => e puts puts e puts e.backtrace.join("\n") end # end of program
Copyright © Thundernet Development Group Inc., 2003. All rights reserved. |