diff --git a/mswp.rb b/mswp.rb index 69a283f..7eae287 100644 --- a/mswp.rb +++ b/mswp.rb @@ -53,20 +53,20 @@ def isDoubted end end - class GameOverException < Exception + class GameOverException < StandardError end - class GameClearException < Exception + class GameClearException < StandardError end def initialize(length, nr_mines) - @map = MDArray.new(length) + @field = MDArray.new(length) @nr_mines = nr_mines - if @nr_mines >= @map.size + if @nr_mines >= @field.size raise ArgumentError.new end - @map.fill { |i| Cell.new } + @field.fill { |i| Cell.new } @active = false end @@ -75,7 +75,7 @@ def isTouched(pos) return end - @map[pos].isTouched + @field[pos].isTouched end def touch(pos) @@ -83,7 +83,7 @@ def touch(pos) setup(pos) end - cell = @map[pos] + cell = @field[pos] if cell.isFlagged || cell.isTouched return end @@ -109,7 +109,7 @@ def toggleFlag(pos) return end - cell = @map[pos] + cell = @field[pos] if cell.isTouched return end @@ -128,7 +128,7 @@ def toggleDoubt(pos) return end - cell = @map[pos] + cell = @field[pos] if cell.isTouched return end @@ -148,12 +148,12 @@ def touchNeighbors(pos) return end - cell = @map[pos] + cell = @field[pos] if ! cell.isTouched return end - nr_flagged_cells = @map.neighbor8_with_index(pos).inject(0) { |sum, (neighbor, i)| + nr_flagged_cells = @field.neighbor8_with_index(pos).inject(0) { |sum, (neighbor, i)| sum + (neighbor.isFlagged ? 1 : 0) } if nr_flagged_cells != cell.getNumberOfNeighborMines @@ -168,19 +168,19 @@ def flagNeighbors(pos) return end - cell = @map[pos] + cell = @field[pos] if ! cell.isTouched return end - nr_untouched_cells = @map.neighbor8_with_index(pos).inject(0) { |sum, (neighbor, i)| + nr_untouched_cells = @field.neighbor8_with_index(pos).inject(0) { |sum, (neighbor, i)| sum + (neighbor.isTouched ? 0 : 1) } if nr_untouched_cells != cell.getNumberOfNeighborMines return end - @map.neighbor8_with_index(pos).each { |(neighbor, i)| + @field.neighbor8_with_index(pos).each { |(neighbor, i)| if ! (neighbor.isTouched || neighbor.isFlagged) neighbor.flag @nr_flagged_cells += 1 @@ -189,13 +189,13 @@ def flagNeighbors(pos) end def neighbors(pos) - @map.neighbor8_with_index(pos).map { |(neighbor, i)| + @field.neighbor8_with_index(pos).map { |(neighbor, i)| [Marshal.load(Marshal.dump(neighbor)).freeze, i] } end def each - @map.each_with_index { |cell, pos| + @field.each_with_index { |cell, pos| yield(Marshal.load(Marshal.dump(cell)).freeze, pos) } end @@ -206,25 +206,25 @@ def each def setup(pos) # 地雷の配置 - (@map.all - [@map[pos]] - (@nr_mines <= @map.size - 3 ** @map.dimension ? - @map.neighbor8_with_index(pos).map { |(cell, i)| cell } : - [])).sort_by { rand }[0...@nr_mines].each { |cell| cell.mine } + (@field.all - [@field[pos]] - (@nr_mines <= @field.size - 3 ** @field.dimension ? + @field.neighbor8_with_index(pos).map { |(cell, i)| cell } : + [])).sort_by { rand }[0...@nr_mines].each { |cell| cell.mine } # 近傍地雷数の計算 - @map.each_with_index { |cell, i| + @field.each_with_index { |cell, i| if ! cell.isMined - cell.setNumberOfNeighborMines(@map.neighbor8_with_index(i).inject(0) { |sum, (neighbor, j)| + cell.setNumberOfNeighborMines(@field.neighbor8_with_index(i).inject(0) { |sum, (neighbor, j)| sum += neighbor.isMined ? 1 : 0 }) end } - @nr_untouched_cells = @map.size + @nr_untouched_cells = @field.size @nr_flagged_cells = 0 @active = true end def __touchNeighbors(pos) - @map.neighbor8_with_index(pos).each { |(cell, i)| + @field.neighbor8_with_index(pos).each { |(cell, i)| touch(i) } end diff --git a/mswp_curses.rb b/mswp_curses.rb index 7912439..4d70fe8 100755 --- a/mswp_curses.rb +++ b/mswp_curses.rb @@ -2,171 +2,217 @@ # -*- coding: utf-8 -*- require 'curses' +require 'matrix' require './mswp.rb' class Cursor - def initialize(length) - @length = length.freeze - @pos = Array.new(length.length, 0) - end + def initialize(size) + @size = size.freeze - def move(dim, delta) - if ! ((0...@length[dim]) === @pos[dim] + delta) - return - end + dim = size.size + @pos = Vector[*([0] * dim)] + end - @pos[dim] += delta + def move(delta) + new_pos = @pos + delta + @pos = new_pos.zip(@size).all? { |p, s| (0...s).include? p } ? new_pos : @pos end - attr_reader :pos + def pos + @pos.to_a + end end -MAP_WIDTH = ARGV[0].to_i -MAP_HEIGHT = ARGV[1].to_i -MAP_DEPTH = ARGV[2].to_i -MAP_HYPER_DEPTH = ARGV[3].to_i -NR_MINES = ARGV[4].to_i - -def printField(ms, cur) - ms.each { |cell, pos| - offset = ((pos == cur.pos) ? - 10 : (pos.each_index.inject(true) { |tmp, i| - (tmp && (pos[i] - cur.pos[i]).abs <= 1) - } ? - 5 : 0)) - - Curses.setpos(pos[2] + (MAP_HEIGHT + 1) * pos[0] + 2, 2 * pos[3] + (MAP_WIDTH + 1) * 2 * pos[1]) - if ms.active && ! cell.isTouched - if cell.isFlagged - Curses.attron(Curses.color_pair(3 + offset)) - Curses.addstr(" !") - Curses.attroff(Curses::A_COLOR) - elsif cell.isDoubted - Curses.attron(Curses.color_pair(4 + offset)) - Curses.addstr(" ?") - Curses.attroff(Curses::A_COLOR) - else - Curses.attron(Curses.color_pair(2 + offset)) - Curses.addstr(" ") - Curses.attroff(Curses::A_COLOR) - end - else - if cell.isMined - Curses.attron(Curses.color_pair(5 + offset)) - Curses.addstr(" *") - Curses.attroff(Curses::A_COLOR) - else - Curses.attron(Curses.color_pair(1 + offset)) - Curses.addstr((cell.getNumberOfNeighborMines == 0) ? - " ." : - sprintf('%2d', cell.getNumberOfNeighborMines)) - Curses.attroff(Curses::A_COLOR) - end +class CursesRenderer + CellWidth = 2 + CellHeight = 1 + HeaderHeight = 2 + MarginBtwDim = 1 + + def initialize(field_hyper_depth, field_depth, field_height, field_width) + @field_hyper_depth = field_hyper_depth + @field_depth = field_depth + @field_height = field_height + @field_width = field_width + + Curses.init_screen + Curses.start_color + Curses.init_pair(1, Curses::COLOR_WHITE, Curses::COLOR_BLACK) + Curses.init_pair(2, Curses::COLOR_BLACK, Curses::COLOR_WHITE) + Curses.init_pair(3, Curses::COLOR_RED, Curses::COLOR_WHITE) + Curses.init_pair(4, Curses::COLOR_BLUE, Curses::COLOR_WHITE) + Curses.init_pair(5, Curses::COLOR_BLACK, Curses::COLOR_RED) + Curses.init_pair(6, Curses::COLOR_WHITE, Curses::COLOR_BLUE) + Curses.init_pair(7, Curses::COLOR_BLACK, Curses::COLOR_CYAN) + Curses.init_pair(8, Curses::COLOR_RED, Curses::COLOR_CYAN) + Curses.init_pair(9, Curses::COLOR_BLUE, Curses::COLOR_CYAN) + Curses.init_pair(10, Curses::COLOR_BLACK, Curses::COLOR_RED) + Curses.init_pair(11, Curses::COLOR_WHITE, Curses::COLOR_GREEN) + Curses.init_pair(12, Curses::COLOR_BLACK, Curses::COLOR_YELLOW) + Curses.init_pair(13, Curses::COLOR_RED, Curses::COLOR_YELLOW) + Curses.init_pair(14, Curses::COLOR_BLUE, Curses::COLOR_YELLOW) + Curses.init_pair(15, Curses::COLOR_RED, Curses::COLOR_BLACK) + Curses.noecho + Curses.curs_set(0) + end + + def cleanup + Curses.close_screen + end + + def print_field(ms, cur_pos) + header = "Mines: #{ms.nr_mines}, Flagged: #{ms.nr_flagged_cells}, Untouched: #{ms.nr_untouched_cells}, Position: (#{cur_pos.reverse.join(', ')})" + curses_print header + " " * (Curses.cols - header.length), + 0, 0, Curses.color_pair(2) + + ms.each do |cell, pos| + y = CellHeight * ((@field_height + MarginBtwDim) * pos[0] + pos[2]) + HeaderHeight + x = CellWidth * ((@field_width + MarginBtwDim) * pos[1] + pos[3]) + + color_offset = case + when pos == cur_pos + 10 + when pos.zip(cur_pos).all? { |p, c| (p - c).abs <= 1 } + 5 + else + 0 + end + + str, attrs = if ms.active + case cell + when :isFlagged.to_proc + [' !', Curses.color_pair(3 + color_offset)] + when :isDoubted.to_proc + [' ?', Curses.color_pair(4 + color_offset)] + when :isTouched.to_proc + [(cell.getNumberOfNeighborMines == 0) ? + ' .' : '%2d' % cell.getNumberOfNeighborMines, + Curses.color_pair(1 + color_offset)] + else + [' ', Curses.color_pair(2 + color_offset)] + end + else + case cell + when :isMined.to_proc + [' *', Curses.color_pair(5 + color_offset)] + else + [(cell.getNumberOfNeighborMines == 0) ? + ' .' : '%2d' % cell.getNumberOfNeighborMines, + Curses.color_pair(1 + color_offset)] + end + end + + curses_print str, y, x, attrs end - } - Curses.setpos(0, 0) - str = "Mines: #{ms.nr_mines}, Flagged: #{ms.nr_flagged_cells}, Untouched: #{ms.nr_untouched_cells}, Position: (#{cur.pos.reverse.join(', ')})" - Curses.attron(Curses.color_pair(2)) - Curses.addstr(str << " " * (Curses.cols - str.length)) - Curses.attroff(Curses::A_COLOR) + Curses.refresh + end + + def print_time(min, sec) + Curses.setpos(1, 0) + Curses.addstr(sprintf('TIME: %02d:%02d', min, sec)) + Curses.refresh + end + + def print_gameover + Curses.setpos(CellHeight * (@field_height + MarginBtwDim) * @field_hyper_depth + 2, 0) + Curses.addstr('Game Over...') + Curses.refresh + end - Curses.refresh + def print_gameclear + Curses.setpos(CellHeight * (@field_height + MarginBtwDim) * @field_hyper_depth + 2, 0) + Curses.addstr('Game Clear!!') + Curses.refresh + end + + private + + def curses_print(str, y, x, attrs) + Curses.setpos(y, x) + Curses.attron(attrs) + Curses.addstr(str) + Curses.attroff(attrs) + end end -Curses.init_screen -Curses.start_color -Curses.init_pair(1, Curses::COLOR_WHITE, Curses::COLOR_BLACK) -Curses.init_pair(2, Curses::COLOR_BLACK, Curses::COLOR_WHITE) -Curses.init_pair(3, Curses::COLOR_RED, Curses::COLOR_WHITE) -Curses.init_pair(4, Curses::COLOR_BLUE, Curses::COLOR_WHITE) -Curses.init_pair(5, Curses::COLOR_BLACK, Curses::COLOR_RED) -Curses.init_pair(6, Curses::COLOR_WHITE, Curses::COLOR_BLUE) -Curses.init_pair(7, Curses::COLOR_BLACK, Curses::COLOR_CYAN) -Curses.init_pair(8, Curses::COLOR_RED, Curses::COLOR_CYAN) -Curses.init_pair(9, Curses::COLOR_BLUE, Curses::COLOR_CYAN) -Curses.init_pair(10, Curses::COLOR_BLACK, Curses::COLOR_RED) -Curses.init_pair(11, Curses::COLOR_WHITE, Curses::COLOR_GREEN) -Curses.init_pair(12, Curses::COLOR_BLACK, Curses::COLOR_YELLOW) -Curses.init_pair(13, Curses::COLOR_RED, Curses::COLOR_YELLOW) -Curses.init_pair(14, Curses::COLOR_BLUE, Curses::COLOR_YELLOW) -Curses.init_pair(15, Curses::COLOR_RED, Curses::COLOR_BLACK) -Curses.noecho -Curses.curs_set(0) - -$ms = MSwp.new([MAP_HYPER_DEPTH, MAP_DEPTH, MAP_HEIGHT, MAP_WIDTH], NR_MINES) -$cur = Cursor.new([MAP_HYPER_DEPTH, MAP_DEPTH, MAP_HEIGHT, MAP_WIDTH]) - -th = Thread.new { +FieldWidth = ARGV[0].to_i +FieldHeight = ARGV[1].to_i +FieldDepth = ARGV[2].to_i +FieldHyperDepth = ARGV[3].to_i +NumberOfMines = ARGV[4].to_i + +renderer = CursesRenderer.new(FieldHyperDepth, FieldDepth, FieldHeight, FieldWidth) + +ms = MSwp.new([FieldHyperDepth, FieldDepth, FieldHeight, FieldWidth], NumberOfMines) +cur = Cursor.new(Vector[FieldHyperDepth, FieldDepth, FieldHeight, FieldWidth]) + +counter_thread = Thread.new do count = 0 - while true - Curses.setpos(1, 0) - Curses.addstr(sprintf('TIME: %02d:%02d', count / 60, count % 60)) - Curses.refresh + loop do + renderer.print_time(count / 60, count % 60) sleep 1 count += 1 end -} +end begin while true - printField($ms, $cur) + renderer.print_field(ms, cur.pos) case Curses.getch when ?q break when ?h - $cur.move(3, -1) + cur.move(Vector[0, 0, 0, -1]) when ?l - $cur.move(3, 1) + cur.move(Vector[0, 0, 0, 1]) when ?k - $cur.move(2, -1) + cur.move(Vector[0, 0, -1, 0]) when ?j - $cur.move(2, 1) + cur.move(Vector[0, 0, 1, 0]) when ?H - $cur.move(1, -1) + cur.move(Vector[0, -1, 0, 0]) when ?L - $cur.move(1, 1) + cur.move(Vector[0, 1, 0, 0]) when ?K - $cur.move(0, -1) + cur.move(Vector[-1, 0, 0, 0]) when ?J - $cur.move(0, 1) + cur.move(Vector[1, 0, 0, 0]) when ?\s - if $ms.isTouched($cur.pos) - $ms.touchNeighbors($cur.pos) + if ms.isTouched(cur.pos) + ms.touchNeighbors(cur.pos) else - $ms.touch($cur.pos) + ms.touch(cur.pos) end when ?f - if $ms.isTouched($cur.pos) - $ms.flagNeighbors($cur.pos) + if ms.isTouched(cur.pos) + ms.flagNeighbors(cur.pos) else - $ms.toggleFlag($cur.pos) + ms.toggleFlag(cur.pos) end when ?? - $ms.toggleDoubt($cur.pos) + ms.toggleDoubt(cur.pos) end - # if $ms.isTouched($cur.pos) - # $ms.flagNeighbors($cur.pos) - # $ms.touchNeighbors($cur.pos) + # if ms.isTouched(cur.pos) + # ms.flagNeighbors(cur.pos) + # ms.touchNeighbors(cur.pos) # end end -rescue MSwp::GameOverException - th.kill - printField($ms, $cur) - Curses.setpos((MAP_HEIGHT + 1) * MAP_HYPER_DEPTH + 2, 0) - Curses.addstr('Game Over...') - Curses.refresh - while Curses.getch != ?q; end -rescue MSwp::GameClearException - th.kill - printField($ms, $cur) - Curses.setpos((MAP_HEIGHT + 1) * MAP_HYPER_DEPTH + 2, 0) - Curses.addstr('Game Clear!!') - Curses.refresh +rescue MSwp::GameOverException, MSwp::GameClearException => e + counter_thread.kill + + renderer.print_field(ms, cur.pos) + case e + when MSwp::GameOverException + renderer.print_gameover + when MSwp::GameClearException + renderer.print_gameclear + end + while Curses.getch != ?q; end end -Curses.close_screen +renderer.cleanup