ruby-go 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +30 -16
- data/bin/rubygo +56 -0
- data/lib/ruby-go.rb +9 -4
- data/lib/ruby-go/board.rb +66 -0
- data/lib/ruby-go/game.rb +135 -0
- data/lib/ruby-go/liberty.rb +11 -0
- data/lib/ruby-go/moves.rb +63 -0
- data/lib/ruby-go/printers/html.rb +76 -0
- data/lib/ruby-go/printers/text.rb +40 -0
- data/lib/ruby-go/stone.rb +50 -0
- data/lib/ruby-go/version.rb +3 -0
- data/test/go_test.rb +158 -136
- data/test/sgf_test.rb +43 -42
- data/test/test_helper.rb +2 -1
- metadata +15 -11
- data/bin/ruby-go.rb +0 -45
- data/lib/board.rb +0 -82
- data/lib/game.rb +0 -116
- data/lib/stone.rb +0 -101
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: '024418870e37d715d73c1ce299b3db4c59b231d8fea65b64870e280b9f439387'
         | 
| 4 | 
            +
              data.tar.gz: 460d7dbde08c15c6aec3e067dcd50c11d1b2dfac31cd708a97826a3b7b585ad1
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: b2871316ce937e2df59f6ec8b912b51bb20cc1b37c3d257e3ddb6be0067f559d7a18feb6d8e292955257d910cfacb36aab945794912f61438d036c0d561269b3
         | 
| 7 | 
            +
              data.tar.gz: bba3a81eac11819591ff71683b413a9679d06e15fc3334b3e874e415ec25886a600272e783f98495a578722ad8fc4c4514089e954cd2bdf73d6046c8e3836555
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,16 +1,30 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 1 | 
            +
            # Ruby Go
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            A gem that implements the rules of go (igo, weiqi, baduk).
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## Use
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ```
         | 
| 8 | 
            +
            game = RubyGo::Game(board: 19)
         | 
| 9 | 
            +
            game.place_black(3,3)
         | 
| 10 | 
            +
            game.place_white(15,15)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            # ...
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            game.pass_black
         | 
| 15 | 
            +
            game.pass_white
         | 
| 16 | 
            +
            ```
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            ### Printers
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            There are a few printers included in the gem, including `TextPrinter` and `HTMLPrinter`.
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            The printer takes an `IO` like object, and can print the game.
         | 
| 23 | 
            +
            ```
         | 
| 24 | 
            +
            printer = RubyGo::TextPrinter.new($stdout)
         | 
| 25 | 
            +
            printer.print_game(game)
         | 
| 26 | 
            +
            ```
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            ### rubygo Command Line Interface
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            The rubygo executable included in the gem is CLI that plays the game of go.
         | 
    
        data/bin/rubygo
    ADDED
    
    | @@ -0,0 +1,56 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'ruby-go'
         | 
| 4 | 
            +
            require 'ruby-go/printers/text'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            game = RubyGo::Game.new
         | 
| 7 | 
            +
            printer = RubyGo::TextPrinter.new($stdout)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Signal.trap("INTERUPT") do
         | 
| 10 | 
            +
              puts "\nGoodbye"
         | 
| 11 | 
            +
              exit 1
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            def prompt_for_turn(game)
         | 
| 15 | 
            +
              loop do
         | 
| 16 | 
            +
                print 'Enter "Pass" or enter coordinates x, y for move (e.g. "4, 4"): '
         | 
| 17 | 
            +
                ans = gets.chomp.downcase.gsub(/\s/, '')
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                return if ans =~ /pass/
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                return ans.split(',').collect {|c| Integer(c)} if ans =~/\d+,\d+/
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            def update_terminal(printer, game)
         | 
| 26 | 
            +
              system "clear"
         | 
| 27 | 
            +
              printer.print_game(game)
         | 
| 28 | 
            +
            end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            update_terminal(printer, game)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            turn = 0
         | 
| 33 | 
            +
            until game.passes >= 2
         | 
| 34 | 
            +
              begin
         | 
| 35 | 
            +
                if turn % 2 == 0
         | 
| 36 | 
            +
                  #black's turn
         | 
| 37 | 
            +
                  puts "Black's turn"
         | 
| 38 | 
            +
                  move = prompt_for_turn(game)
         | 
| 39 | 
            +
                  move ? game.place_black(*move) : game.black_pass
         | 
| 40 | 
            +
                else
         | 
| 41 | 
            +
                  #white's turn
         | 
| 42 | 
            +
                  puts "White's turn"
         | 
| 43 | 
            +
                  move = prompt_for_turn(game)
         | 
| 44 | 
            +
                  move ? game.place_white(*move) : game.white_pass
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                update_terminal(printer, game)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                turn += 1
         | 
| 50 | 
            +
              rescue RubyGo::Game::IllegalMove => e
         | 
| 51 | 
            +
                puts e.message
         | 
| 52 | 
            +
                next
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            puts "Game over"
         | 
    
        data/lib/ruby-go.rb
    CHANGED
    
    | @@ -1,5 +1,10 @@ | |
| 1 | 
            -
            require_relative 'board'
         | 
| 2 | 
            -
            require_relative 'game'
         | 
| 3 | 
            -
            require_relative 'stone'
         | 
| 4 | 
            -
             | 
| 5 1 | 
             
            require 'sgf'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'ruby-go/liberty'
         | 
| 4 | 
            +
            require_relative 'ruby-go/stone'
         | 
| 5 | 
            +
            require_relative 'ruby-go/board'
         | 
| 6 | 
            +
            require_relative 'ruby-go/moves'
         | 
| 7 | 
            +
            require_relative 'ruby-go/game'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module RubyGo
         | 
| 10 | 
            +
            end
         | 
| @@ -0,0 +1,66 @@ | |
| 1 | 
            +
            module RubyGo
         | 
| 2 | 
            +
              class Board
         | 
| 3 | 
            +
                attr_reader :rows, :size
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(size)
         | 
| 6 | 
            +
                  @rows = Array.new(size) { Array.new(size) { Liberty.new } }
         | 
| 7 | 
            +
                  @size = size
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def empty?
         | 
| 11 | 
            +
                  rows.flatten.all?(&:empty?)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def at(x, y)
         | 
| 15 | 
            +
                  rows[y][x]
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def around(x, y)
         | 
| 19 | 
            +
                  intersections = []
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  intersections << at(x-1, y) unless x == 0
         | 
| 22 | 
            +
                  intersections << at(x+1, y) unless x == (size - 1)
         | 
| 23 | 
            +
                  intersections << at(x, y-1) unless y == 0
         | 
| 24 | 
            +
                  intersections << at(x, y+1) unless y == (size - 1)
         | 
| 25 | 
            +
                  intersections
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def remove(stone)
         | 
| 29 | 
            +
                  return if stone.empty?
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  x, y = stone.to_coord
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  rows[y][x] = Liberty.new
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def place(stone)
         | 
| 37 | 
            +
                  x, y = stone.to_coord
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  rows[y][x] = stone
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def liberties(stone)
         | 
| 43 | 
            +
                  libs = []
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  group_of(stone).each do |stn|
         | 
| 46 | 
            +
                    libs += around(*stn.to_coord).select(&:empty?)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  libs.uniq.length
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def group_of(stone, stones = [])
         | 
| 53 | 
            +
                  return stones if stones.include?(stone)
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  stones << stone
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  around(*stone.to_coord).each do |intersection|
         | 
| 58 | 
            +
                    next if intersection.empty?
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    group_of(intersection, stones) if intersection.color == stone.color
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  stones
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
            end
         | 
    
        data/lib/ruby-go/game.rb
    ADDED
    
    | @@ -0,0 +1,135 @@ | |
| 1 | 
            +
            module RubyGo
         | 
| 2 | 
            +
              class Game
         | 
| 3 | 
            +
                attr_reader :board, :moves
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(board: 19)
         | 
| 6 | 
            +
                  @board = Board.new(board)
         | 
| 7 | 
            +
                  @moves = Moves.new
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                private :moves
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def save(name="my_go_game")
         | 
| 13 | 
            +
                  tree = SGF::Parser.new.parse(to_sgf)
         | 
| 14 | 
            +
                  tree.save(name + '.sgf')
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def to_sgf
         | 
| 18 | 
            +
                  sgf = "(;GM[1]FF[4]CA[UTF-8]AP[jphager2]SZ[#{board.size}]PW[White]PB[Black]"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  moves.each do |move|
         | 
| 21 | 
            +
                    sgf << move.played.to_sgf
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  sgf << ')'
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def place_black(x, y)
         | 
| 28 | 
            +
                  play(Stone.new(x, y, :black))
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def place_white(x, y)
         | 
| 32 | 
            +
                  play(Stone.new(x, y, :white))
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def black_pass
         | 
| 36 | 
            +
                  pass(:black)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def white_pass
         | 
| 40 | 
            +
                  pass(:white)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def undo
         | 
| 44 | 
            +
                  move = moves.pop
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  board.remove(move.played)
         | 
| 47 | 
            +
                  move.captures.each do |stone|
         | 
| 48 | 
            +
                    board.place(stone)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def passes
         | 
| 53 | 
            +
                  moves.pass_count
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def captures
         | 
| 57 | 
            +
                  moves.capture_count
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                private
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def pass(color)
         | 
| 63 | 
            +
                  moves.pass(NullStone.new(color))
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def play(stone)
         | 
| 67 | 
            +
                  check_illegal_placement!(stone)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  board.place(stone)
         | 
| 70 | 
            +
                  moves.play(stone)
         | 
| 71 | 
            +
                  record_captures!(stone)
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  check_illegal_suicide!(stone)
         | 
| 74 | 
            +
                  check_illegal_ko!(stone)
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def check_illegal_placement!(stone)
         | 
| 78 | 
            +
                  coord = stone.to_coord
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  if coord.any? { |i| i < 0 || i >= board.size }
         | 
| 81 | 
            +
                    raise(
         | 
| 82 | 
            +
                      Game::IllegalMove,
         | 
| 83 | 
            +
                      "You cannot place a stone off the board."
         | 
| 84 | 
            +
                    )
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  intersection = board.at(*coord)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  unless intersection.empty?
         | 
| 90 | 
            +
                    raise(
         | 
| 91 | 
            +
                      Game::IllegalMove,
         | 
| 92 | 
            +
                      "You cannot place a stone on top of another stone."
         | 
| 93 | 
            +
                    )
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def check_illegal_ko!(stone)
         | 
| 98 | 
            +
                  last_move = moves.prev
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  return unless last_move
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  if last_move.captures == [stone] && moves.current.captures.one?
         | 
| 103 | 
            +
                    undo
         | 
| 104 | 
            +
                    raise IllegalMove,
         | 
| 105 | 
            +
                      "You cannot capture the ko, play a ko threat first"
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def check_illegal_suicide!(stone)
         | 
| 110 | 
            +
                  if board.liberties(stone).zero?
         | 
| 111 | 
            +
                    undo
         | 
| 112 | 
            +
                    raise IllegalMove, "You cannot play a suicide."
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                def record_captures!(stone)
         | 
| 117 | 
            +
                  stones_around = board.around(*stone.to_coord).reject(&:empty?)
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  captures = stones_around
         | 
| 120 | 
            +
                               .reject {| stn| stn.color == stone.color }
         | 
| 121 | 
            +
                               .select { |stn| @board.liberties(stn).zero? }
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  captures.map {|stone| @board.group_of(stone)}
         | 
| 124 | 
            +
                    .flatten.uniq.each {|stone| capture_stone(stone)}
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def capture_stone(stone)
         | 
| 128 | 
            +
                  moves.capture(stone)
         | 
| 129 | 
            +
                  board.remove(stone)
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                class IllegalMove < StandardError
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
              end
         | 
| 135 | 
            +
            end
         | 
| @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            module RubyGo
         | 
| 2 | 
            +
              class Moves
         | 
| 3 | 
            +
                attr_reader :internal_moves, :capture_count, :pass_count
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize
         | 
| 6 | 
            +
                  @internal_moves = []
         | 
| 7 | 
            +
                  @pass_count = 0
         | 
| 8 | 
            +
                  @capture_count = { black: 0, white: 0 }
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def play(played)
         | 
| 12 | 
            +
                  @pass_count += 0
         | 
| 13 | 
            +
                  internal_moves << Move.new(played)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def pass(pass)
         | 
| 17 | 
            +
                  @pass_count += 1
         | 
| 18 | 
            +
                  internal_moves << Move.new(pass)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def each(&block)
         | 
| 22 | 
            +
                  internal_moves.each(&block)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def prev
         | 
| 26 | 
            +
                  internal_moves[-2]
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def current
         | 
| 30 | 
            +
                  internal_moves[-1]
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def pop
         | 
| 34 | 
            +
                  move = internal_moves.pop
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  move.captures.each do |stone|
         | 
| 37 | 
            +
                    capture_count[stone.color] -= 1
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  @pass_count -= 1 if move.empty?
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  move
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def capture(stone)
         | 
| 46 | 
            +
                  current.captures << stone
         | 
| 47 | 
            +
                  capture_count[stone.color] += 1
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              class Move
         | 
| 52 | 
            +
                attr_reader :played, :captures
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def initialize(played)
         | 
| 55 | 
            +
                  @played = played
         | 
| 56 | 
            +
                  @captures = []
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def empty?
         | 
| 60 | 
            +
                  played.empty?
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| @@ -0,0 +1,76 @@ | |
| 1 | 
            +
            require 'erb'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RubyGo
         | 
| 4 | 
            +
              class HTMLPrinter
         | 
| 5 | 
            +
                LETTERS = ('A'..'Z').to_a.freeze
         | 
| 6 | 
            +
                COLORS = { black: 'black stone', white: 'white stone', empty: 'liberty' }.freeze
         | 
| 7 | 
            +
                TEMPLATE = ERB.new(<<~ERB).freeze
         | 
| 8 | 
            +
                  <style>
         | 
| 9 | 
            +
                  .board { max-width: 100%; padding: 16px; }
         | 
| 10 | 
            +
                  .row { display: flex; margin: 0; }
         | 
| 11 | 
            +
                  .intersection { display: inline-block; height: 32px; width: 32px; border-top: 2px solid black; border-left: 2px solid black; }
         | 
| 12 | 
            +
                  .intersection:last-child { border-top-width: 0px; height: 34px; }
         | 
| 13 | 
            +
                  .row:last-child .intersection { border-left-width: 0px; width: 34px; }
         | 
| 14 | 
            +
                  .row:last-child .intersection:last-child { width: 2px; height: 2px; background: black; }
         | 
| 15 | 
            +
                  .stone, .liberty { display: inline-block; width: 30px; height: 30px; border-radius: 100%; margin: 0; position: relative; top: -18px; left: -18px; }
         | 
| 16 | 
            +
                  .stone { border: 2px solid black; }
         | 
| 17 | 
            +
                  .black.stone { background: black; }
         | 
| 18 | 
            +
                  .white.stone { background: white; }
         | 
| 19 | 
            +
                  .liberty:hover { border: 2px solid red; }
         | 
| 20 | 
            +
                  .row-num, .column-num { display: inline-block; width: 34px; height: 34px; margin: 0; position: relative; }
         | 
| 21 | 
            +
                  .row-num { top: -0.5em; }
         | 
| 22 | 
            +
                  .column-num { left: -0.3em; }
         | 
| 23 | 
            +
                  </style>
         | 
| 24 | 
            +
                  <div class="game">
         | 
| 25 | 
            +
                    <div class="board">
         | 
| 26 | 
            +
                      <div class="row">
         | 
| 27 | 
            +
                        <div class="column-num row-num"></div>
         | 
| 28 | 
            +
                        <% size.times do |i| %>
         | 
| 29 | 
            +
                        <div class="column-num"><%= letters[i] %></div>
         | 
| 30 | 
            +
                        <% end %>
         | 
| 31 | 
            +
                      </div>
         | 
| 32 | 
            +
                      <% rows.each_with_index do |row, i| %>
         | 
| 33 | 
            +
                      <div class="row">
         | 
| 34 | 
            +
                        <div class="row-num"><%= letters[i] %></div>
         | 
| 35 | 
            +
                        <% row.each do |stn| %>
         | 
| 36 | 
            +
                          <div class="intersection">
         | 
| 37 | 
            +
                            <div class="<%= colors[stn.color] %>"></div>
         | 
| 38 | 
            +
                          </div>
         | 
| 39 | 
            +
                        <% end %>
         | 
| 40 | 
            +
                      </div>
         | 
| 41 | 
            +
                      <% end %>
         | 
| 42 | 
            +
                    </div>
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    <div class="info">
         | 
| 45 | 
            +
                      <table>
         | 
| 46 | 
            +
                        <tbody>
         | 
| 47 | 
            +
                          <tr>
         | 
| 48 | 
            +
                            <th>Prisoners</th>
         | 
| 49 | 
            +
                            <th>White</th>
         | 
| 50 | 
            +
                            <td><%= captures[:black] %></td>
         | 
| 51 | 
            +
                            <th>Black</th>
         | 
| 52 | 
            +
                            <td><%= captures[:white] %></td>
         | 
| 53 | 
            +
                          </tr>
         | 
| 54 | 
            +
                        </tbody>
         | 
| 55 | 
            +
                      </table>
         | 
| 56 | 
            +
                    </div>
         | 
| 57 | 
            +
                  </div>
         | 
| 58 | 
            +
                ERB
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                attr_reader :io
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def initialize(io)
         | 
| 63 | 
            +
                  @io = io
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def print_game(game)
         | 
| 67 | 
            +
                  html = TEMPLATE.result_with_hash(
         | 
| 68 | 
            +
                    size: game.board.size,
         | 
| 69 | 
            +
                    captures: game.captures,
         | 
| 70 | 
            +
                    rows: game.board.rows,
         | 
| 71 | 
            +
                    colors: COLORS, letters: LETTERS
         | 
| 72 | 
            +
                  )
         | 
| 73 | 
            +
                  io.write(html)
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
            end
         |