Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 29 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
29
Dung lượng
184 KB
Nội dung
ANSWER 11. SOKOBAN 139 sokoban/objectified.rb module Sokoban class Crate def to_s ' o' end end class Person def to_s ' @' end end end Then we get to the meat of the program, which is the Level class: sokoban/objectified.rb module Sokoban class Level attr_reader :moves def initialize(str) @grid = str.split("\n").map{|ln| ln.split(//).map{|c| Tile.create(c) } } throw SokobanError.new(' No player found on level' ) if !player_index throw SokobanError.new(' No challenge!' ) if solved? @moves = 0 end def [](r, c) @grid[r][c] end def to_s @grid.map{|row| row.join }.join("\n") end # returns a 2-element array with the row and column of the # player ' s position, respectively def player_index @grid.each_index do |row| @grid[row].each_index do |col| if @grid[row][col].respond_to?(:resident) && Person === @grid[row][col].resident return [row, col] end end end nil end def solved? # a level is solved when every Storage tile has a Crate @grid.flatten.all? {|tile| !(Storage === tile) || tile.has_crate? } end def move(dir) if [NORTH,SOUTH,EAST,WEST].include?(dir) Report erratum ANSWER 11. SOKOBAN 140 pos = player_index target = @grid[pos[0] + dir[0]][pos[1] + dir[1]] if Floor === target if Crate === target.resident indirect_target = @grid[pos[0] + 2*dir[0]][pos[1] + 2*dir[1]] if Floor === indirect_target && !indirect_target.resident @grid[pos[0] + 2*dir[0]][pos[1] + 2*dir[1]] << @grid[pos[0] + dir[0]][pos[1] + dir[1]].clear @grid[pos[0] + dir[0]][pos[1] + dir[1]] << @grid[pos[0]][pos[1]].clear return @moves += 1 end else @grid[pos[0] + dir[0]][pos[1] + dir[1]] << @grid[pos[0]][pos[1]].clear return @moves += 1 end end end nil end end end Level objects build a @grid of T i l e objects in initialize( ) to manage their state. The methods [ ] and to_s( ) provide indexing and display for the @grid. You can also easily locate the Person object in the @grid wit h player_index( ) and see whether the Level is complete with solved?( ). The final method of Level is move( ), which works roughly the same as Dennis’s version. It finds the player and checks the square in the direc- tion the player is trying to move. If a crate is found there, it also checks the square behind that one. The rest of Dave’s solution is an inter act i ve user interface he provided for it: sokoban/objectified.rb module Sokoban # command-line interface def self.cli(levels_file = ' sokoban_levels.txt' ) cli_help = <<- end Dave' s Cheap Ruby Sokoban (c) Dave Burt 2004 @ is you + is you standing on storage # is a wall . is empty storage o is a crate Report erratum ANSWER 11. SOKOBAN 141 * is a crate on storage Move all the crates onto storage. to move: n/k | w/h -+- e/l | s/j to restart the level: r to quit: x or q or ! to show this message: ? You can queue commands like this: nwwwnnnwnwwsw end cli_help.gsub!(/\t+/,' : ' ) puts cli_help File.read(levels_file).split( "\n\n"). each_with_index do |level_string, level_index| level = Level.new(level_string) while !level.solved? do puts level print ' L:' + (level_index+1).to_s + ' M:' + level.moves.to_s + ' > ' gets.split(//).each do |c| case c when ' w' , ' h' level.move(WEST) when ' s' , ' j' level.move(SOUTH) when ' n' , ' k' level.move(NORTH) when ' e' , ' l' level.move(EAST) when ' r' level = Level.new(level_string) when ' q' , ' x' , ' !' puts ' Bye!' exit when ' d' # debug - ruby prompt print ' ruby> ' begin puts eval(gets) rescue puts $! end when ' ?' puts cli_help when "\n", "\r", "\t", " " Report erratum ANSWER 11. SOKOBAN 142 # ignore whitespace else puts "Invalid command: ' #{c}' " puts cli_help end end end puts "\nCongratulations - you beat level #{level_index + 1}!\n\n" end end end if $0 == __FILE__ Sokoban::cli end That’s not as scary as it looks. The first half is a String of instructions printed to the user, and the second half is just a case statement that matches user input to all the methods we’ve been examining. As you can see, this interface could be replaced with GUI method calls while still leveraging the underlying system. This wouldn’t be any more work than building the command-line interface was. Saving Your Fingers This challenge touches on an i nteresting aspect of software design: interface. With a game, interface is critical. Dennis Ranke’s and Dave Burt’s games read line-oriented input, requiring you to push Enter (Return) to send a move. Although they do allow you to queue up a long line of moves, this tires my poor little fingers out, especially on involved levels. That begs the question, why did they use this approach? Portability would be my guess. Reading a single character from a ter- minal interface can get tricky, depending on which operating system you are running on. Here’s how I do it on Unix: def get_character state = ‘stty -g‘ begin system "stty raw -echo cbreak" @input.getc ensure system "stty #{state}" end end Report erratum ANSWER 11. SOKOBAN 143 Here’s one way you might try the same thing on Windows: def read_char require "Win32API" Win32API.new("crtdll", "_getch", [], "L").Call end If you want your game to run on both, you may need to write code to detect the platform and use the proper method. Here’s one way you might accomplish that: begin require "Win32API" def read_char Win32API.new( "crtdll", "_getch", [], "L").Call end rescue LoadError def read_char state = ‘stty -g‘ begin system "stty raw -echo cbreak" @input.getc ensure system "stty #{state}" end end end That doesn’t cover every platform, but I believe it will work with Win- dows and most Unix flavors (including Mac OS X). That may be enough for some purposes. Another way to handle this would be to use t he Curses library.Curses is standard Ruby but unfortunately is not so standard in the Windows world. A great advantage to this approach is being able to use the arrow keys, which makes for the best interface, I think. Interface work can quickly g et neck deep in external dependencies, it seems. Since games are largely defined by their interfaces, that makes for some complex choices. Maybe we should hope for a Swing-like addi- tion to the Ruby Standard Library sometime in the future. Additional Exercises 1. Modify your solution’s interface so it responds immediately to indi- vidual keystrokes ( without pressing Return). Report erratum ANSWER 11. SOKOBAN 144 2. Add a move counter, and modify your solution to track a lowest- moves score for each level. 3. Add a save-and-restore feature to your game to allow players to suspend play and resume the game at a later time. 4. Solve levels one through ten of Sokoban. Report erratum ANSWER 12. CROSSWORDS 145 Answer 12 From page 29 Crosswords Let’s break down a clean solution from Jim D. Freeze: crosswords/clean.rb class CrossWordPuzzle CELL_WIDTH = 6 CELL_HEIGHT = 4 attr_accessor :cell_width, :cell_height def initialize(file) @file = file @cell_width = CELL_WIDTH @cell_height = CELL_HEIGHT build_puzzle end private def build_puzzle parse_grid_file drop_outer_filled_boxes create_numbered_grid end end Nothing tricky there. First, initialize some constants and variables. After that, the private method build_puzzle( ) outlines the process. Let’s dig deeper into each of those steps. (In the code extracts that follow, parse_grid_file( ), drop_outer_filled_boxes( ), and create_numbered_grid( ) are all private methods of class CrossWordPuzzle. crosswords/clean.rb def parse_grid_file @grid = File.read(@file).split(/\n/) @grid.collect! { |line| line.split } @grid_width = @grid.first.size @grid_height = @grid.size end Report erratum ANSWER 12. CROSSWORDS 146 Step one: read the layout file, break it down by row at each \n character and by square at each space—this solution requires the spaces from t he quiz description—and find the dimensions of the puzzle. crosswords/clean.rb def drop_outer_filled_boxes loop { changed = _drop_outer_filled_boxes(@grid) changed += _drop_outer_filled_boxes(t = @grid.transpose) @grid = t.transpose break if 0 == changed } end def _drop_outer_filled_boxes(ary) changed = 0 ary.collect! { |row| r = row.join changed += 1 unless r.gsub!(/^X|X$/, ' ' ).nil? changed += 1 unless r.gsub!(/X | X/, ' ' ).nil? r.split(//) } changed end These two methods handle step two, dropping filled border squares. Jim uses a simple transpose( ) to perform a two-dimensional search and replace. More than one submission capitalized on this technique. The search-and-replace logic is twofold: Turn all X s at the beginning or end of the line into spaces, and turn all X s n ext to spaces into spaces. Repeat this until there are n o more changes. This causes the edges to creep in until all filled border squares have been eliminated. Spring Cleaning I removed a d uplicate grid from create_numbered_gr i d( ) with a transpose- operate-transpose trick I learned earlier from drop_outer_filled_boxes in this same solution. crosswords/clean.rb def create_numbered_grid mark_boxes(@grid) mark_boxes(t = @grid.transpose) @grid = t.transpose count = ' 0' @numbered_grid = [] @grid.each_with_index { |row, i| r = [] row.each_with_index { |col, j| r << case col when /#/ then count.succ!.dup else col end } @numbered_grid << r } end Report erratum ANSWER 12. CROSSWORDS 147 # place ' #' in boxes to be numbered def mark_boxes(grid) grid.collect! { |row| r = row.join r.gsub!(/([X ])([\ #_]{2,})/) { "#{$1}##{$2[1 1]}" } r.gsub!(/^([\#_]{2,})/) { |m| m[0]=?#; m } r.split(//) } end Here’s the third step, numbering squares. The approach h ere is much the same as step two. A combination of transpose( ) and gsub!( ) is used to mark squares at th e beginning of words with a number sign. Words are defined as a run of number sign and/or underscore characters at the beginning of a line or after a filled box or open space. With num- ber signs in place, it’s a simple matter to replace them with an actual number. Now that the grid has been doctored into the desired format, we need to wrap cells in borders and space and then stringify them. Here’s the code for that. (Again, t hese are methods of CrossWordPuzzle.) Spring Cleaning I switched both calls to sprintf( ) in cell( ) to use the same format String. Both calls were using identical formatting but building it different ways. I thought using the same format String would make that easier to understand. crosswords/clean.rb def cell(data) c = [] case data when ' X' @cell_height.times { c << [' #' ] * @cell_width } when ' ' @cell_height.times { c << [' ' ] * @cell_width } when /\d/ tb = [' #' ] * @cell_width n = sprintf("#%-#{@cell_width-2}s#", data).split(//) m = sprintf( "#%-#{@cell_width-2}s#", ' ' ).split(//) c << tb << n (@cell_height-3).times { c << m } c << tb when ' _' tb = [' #' ] * @cell_width m = [' #' ] + [' ' ]*(@cell_width-2) + [' #' ] c << tb (@cell_height-2).times { c << m } c << tb end c end def overlay(sub, mstr, x, y) sub.each_with_index { |row, i| Report erratum ANSWER 12. CROSSWORDS 148 row.each_with_index { |data, j| mstr[y+i][x+j] = data unless ' #' == mstr[y+i][x+j] } } end def to_s puzzle_width = (@cell_width-1) * @grid_width + 1 puzzle_height = (@cell_height-1) * @grid_height + 1 s = Array.new(puzzle_height) { Array.new(puzzle_width) << [] } @numbered_grid.each_with_index { |row, i| row.each_with_index { |data, j| overlay(cell(data), s, j*(@cell_width-1), i*(@cell_height-1)) } } s.collect! { |row| row.join }.join( "\n") end The method to_s( ) drives the conversion process. It walks the doctored- up grid calling cell( ) to do the formatting and overlay( ) to place it in the puzzle. cell( ) adds number sign bor ders and space as defined by the quiz, based on the cell type it is called on. overlay( ) happily draws cells. However, it’s called with placements close enough together to overlay the borders, reducing them to a single line. This “collapsing borders” technique is common in many aspects of pro- gramming. Examine the output of the mysql command-line tool, GNU Chess, or a hundred other tools. It’s also common for GUI libraries to combine borders of neighboring elements. With an Array of the entire puzzle assembled, to_s( ) finishes wi th few calls to join( ). The “main” program combines the build and display: crosswords/clean.rb cwp = CrossWordPuzzle.new(ARGV.shift) puts cwp.to_s Passive Building Now I want t o examine another solution, by Trans Onoma. This one is a little trickier to figure out, but it uses a pretty clever algorit hm. The following code slowly builds up the board, with only the knowledge Report erratum [...]... winner A rating is first the rank of the type of hand and then the rank of the face of all five cards used in the hand That handles breaking ties (a pair of kings beats a pair of tens) and “kickers” all in one Array Report erratum 164 A NSWER 14 T EXAS H OLD ’ EM Finally we have the private helpers used in all the hand matching Here’s the tiny last bit of code to implement the quiz interface: texas_holdem/texas_hold_em.rb... like we need to get under the hood of that second class If Board is the programmatic representation of the layout, Puzzle represents the answer Puzzle.initialize( ) just builds an Array of Strings the size of the square-expanded layout All of these Strings are initialized to a run of periods Then we get to push( ) That was one of those two methods that seemed to do a lot of the magic in build( ) This method... The idea here is to match numbers against the front of the phone number, passing the matched words and what’s left of the String down recursively, until there is nothing left to match The method returns an Array of chunks, each of which is an Array of all the words that can be used at that point For example, a small part of the search results for the quiz example shows that the number could start with... pair—aces over kings.” 2 Use as much of your solution code as possible to make a two player game of Texas hold’em You can find complete rules of play at http://texasholdem.omnihosts.net/pokerrules.shtml Report erratum 165 A NSWER 15 S OLITAIRE C IPHER Answer From page 36 15 Solitaire Cipher $ ruby solitaire.rb "CLEPK HHNIY CFPWH FDFEH" YOURC IPHER ISWOR KINGX $ ruby solitaire.rb "ABVAW LWZSY OORYK DUPVH"... over the last two lines of the quiz There’s nothing inherently difficult about this quiz It’s really just coding to a specification Knowing that, our focus needs to be accuracy A great way to achieve that is to use unit tests to validate the process Another advantage to this approach is that the quiz itself already gave us a handful of test cases Testing a Cipher The first part of the quiz describes encryption... scoring system Because of that, we don’t need a very complex idea of cards Even hands themselves can be just an Array of cards Here’s the setup: texas_holdem/texas_hold_em.rb require "enumerator" Card = Struct.new(:face, :suit) class Hand FACE_ORDER = %w{A K Q J T 9 8 7 6 5 4 3 2} HAND_ORDER = [ "Royal Flush", "Straight Flush", "Four of a Kind", "Full House", "Flush", "Straight", "Three of a Kind", "Two... more consistent 36 Notice Report erratum 155 A NSWER 13 1-800-THE -QUIZ a digit sequence against the beginning of a number If it matches, we want it to return what’s left of the original number: 1_800_the _quiz/ phone_words.rb def self.match( number, digits ) if number[0, digits.length] == digits number[digits.length -1] else nil end end With that, we are finally ready to search: 1_800_the _quiz/ phone_words.rb... completely or at least mostly solved by the proper sort Poker hands are one of those problems Ruby s Sorting Tricks A couple of sorting niceties in Ruby can make complex sorts a lot easier Let’s talk a little about those before we dig into the code that uses them First, if you’re not familiar with sort_by( ), now is a great time to fix that: $ ruby -e ' p %w{aardvark bat catfish}.sort_by { |str| str.length }... elements You might specify the size of a String, for example Behind the scenes, the elements are replaced with the result of the code block you passed, sorted, and then switched back to the original elements and returned.38 One other useful trick in Ruby is that Arrays themselves are sortable, and they order themselves by comparing each of their child elements in turn: $ ruby -e ' p [[1, 5, 1], [1, 2, 3]].sort... should be used as the width and height of output cells 2 Enhance your program so that a list of clues can follow the board diagram in the input Number and print these clues after the completed board, in two columns Report erratum 152 A NSWER 13 1-800-THE -QUIZ Answer From page 31 13 1-800-THE -QUIZ Some problems are just easier to express with recursion For me, this is one of those problems If you’re not familiar . erratum ANSWER 13. 1-800-THE -QUIZ 1 56 a digit sequence against the beginning of a number. If it matches, we want it to return what’s left of the original number: 1_800_the _quiz/ phone_words.rb def. fruits of our labor. A caller of this method provides a phone number and receives ready-to-print w ord replacements. Here’s the last bit of code that implements the quiz interface: 1_800_the _quiz/ phone_words.rb if. square-expanded layout. All of these Strings ar e init i alized to a run of periods. Then we get to push( ). That was one of those two methods that seemed to do a lot of the magic in build( ).