From fec2531e86f706f76cad2cf4db342288e10b8618 Mon Sep 17 00:00:00 2001 From: Alex Weisberger Date: Sun, 30 Jul 2017 00:01:27 -0400 Subject: [PATCH] Split sections of the UI into views --- lib/freecell/card.rb | 20 ++- lib/freecell/ncurses_ui.rb | 320 ++++++++++++++++++++----------------- 2 files changed, 180 insertions(+), 160 deletions(-) diff --git a/lib/freecell/card.rb b/lib/freecell/card.rb index 621b586..db6d6e3 100644 --- a/lib/freecell/card.rb +++ b/lib/freecell/card.rb @@ -22,20 +22,18 @@ def red? %i[hearts diamonds].include?(suit) end + def color + case suit + when :hearts, :diamonds + :red + when :spades, :clubs + :black + end + end + def opposite_color?(other) red = %i[hearts diamonds] red.include?(suit) ^ red.include?(other.suit) end end - - # Used for printing - class EmptyCard - def black? - false - end - - def red? - false - end - end end diff --git a/lib/freecell/ncurses_ui.rb b/lib/freecell/ncurses_ui.rb index cc0e600..a33088a 100644 --- a/lib/freecell/ncurses_ui.rb +++ b/lib/freecell/ncurses_ui.rb @@ -2,108 +2,122 @@ require_relative 'input_state_machine.rb' module Freecell - # Commandline UI - class NCursesUI - BLACK_CARD_COLOR_PAIR_ID = 1 - SELECTED_BLACK_CARD_COLOR_PAIR_ID = 2 - SELECTED_RED_CARD_COLOR_PAIR_ID = 3 + # Helpers for drawing + module CursesExtensions + def mvaddstr(y, x, str) + Curses.setpos(y, x) + Curses.addstr(str) + end + end - def initialize - @curr_y = 0 - @input_sm = InputStateMachine.new + # Colors + class Colors + BLACK_CARD = 1 + SELECTED_BLACK_CARD = 2 + SELECTED_RED_CARD = 3 + + def self.setup + Curses.start_color + [ + [BLACK_CARD, Curses::COLOR_CYAN, Curses::COLOR_BLACK], + [SELECTED_BLACK_CARD, Curses::COLOR_CYAN, Curses::COLOR_BLUE], + [SELECTED_RED_CARD, Curses::COLOR_WHITE, Curses::COLOR_BLUE] + ].each { |a| Curses.init_pair(*a) } end - def setup - Curses.init_screen - Curses.cbreak - Curses.noecho - Curses.nonl - Curses.curs_set(0) - setup_color - ensure - Curses.close_screen + def black_card_color + Curses.color_pair(BLACK_CARD) end - def render(game_state) - Curses.clear - render_top_area(game_state) - advance_y(by: 3) - render_cascades(game_state) - advance_y(by: 1) - render_bottom_area(game_state) - Curses.refresh - reset_state + def black_selected_card_color + Curses.color_pair(SELECTED_BLACK_CARD) end - def parse_input - command = @input_sm.handle_ch(Curses.getch) - return unless command - case command.type - when :quit - exit - else - command + def red_selected_card_color + Curses.color_pair(SELECTED_RED_CARD) + end + end + + # Base view + class BaseView + include CursesExtensions + + def initialize(y) + @y = y + @colors = Colors.new + end + + def advance_y(by:) + @y += by + Curses.setpos(@y, 0) + end + end + + # Renders a card + class CardView + def initialize(card) + @card = card + @colors = Colors.new + end + + def render(selected_cards) + with_card_coloring(@card, selected_cards) do + Curses.addstr(card_string(@card)) end end private - def setup_color - Curses.start_color - Curses.init_pair( - BLACK_CARD_COLOR_PAIR_ID, - Curses::COLOR_CYAN, - Curses::COLOR_BLACK - ) - Curses.init_pair( - SELECTED_BLACK_CARD_COLOR_PAIR_ID, - Curses::COLOR_CYAN, - Curses::COLOR_BLUE - ) - Curses.init_pair( - SELECTED_RED_CARD_COLOR_PAIR_ID, - Curses::COLOR_WHITE, - Curses::COLOR_BLUE - ) - end - - def reset_state - @curr_y = 0 + def card_string(card) + if card.rank < 10 + " #{card.rank}#{card.suit.to_s[0]}" + else + "#{card.rank}#{card.suit.to_s[0]}" + end end - def render_top_area(game_state) - Curses.addstr('space') - Curses.setpos(@curr_y, 39) - Curses.addstr('enter') - advance_y(by: 1) - render_free_cells(game_state) - Curses.setpos(@curr_y, 21) - Curses.addstr('=)') - Curses.setpos(@curr_y, 24) - render_foundations(game_state) + def with_card_coloring(card, selected_cards) + attr = color_for_card(card, selected_cards) + Curses.attron(attr) if attr + yield card + Curses.attroff(attr) if attr end - def render_bottom_area(game_state) - with_highlight_coloring { Curses.addstr('q') } - Curses.addstr('uit') + def color_for_card(card, selected_cards) + selected = selected_cards.include?(card) if selected_cards + case card.color + when :red + @colors.red_selected_card_color if selected + when :black + black_color(selected) + end + end - Curses.setpos(@curr_y, 5) - with_highlight_coloring { Curses.addstr('u') } - Curses.addstr('ndo') + def black_color(selected) + if selected + @colors.black_selected_card_color + else + @colors.black_card_color + end + end + end - Curses.setpos(@curr_y, 10) - with_highlight_coloring { Curses.addstr('?') } - Curses.addstr('=help') - Curses.setpos(@curr_y, 28) - Curses.addstr("#{game_state.num_moves} moves,") - Curses.setpos(@curr_y, 37) - Curses.addstr("#{game_state.num_undos} undos") + # Free cells and Cascades + class TopView < BaseView + def render(game_state) + mvaddstr(@y, 0, 'space enter') + advance_y(by: 1) + render_free_cells(game_state) + mvaddstr(@y, 21, '=)') + Curses.setpos(@y, 24) + render_foundations(game_state) end + private + def render_free_cells(game_state) draw_occupied_free_cells(game_state) draw_empty_free_cells(game_state) - Curses.setpos(@curr_y + 1, 0) game_state.free_cells.each_index do |i| Curses.addstr(" #{i_to_free_cell_letter(i)} ") end @@ -112,40 +126,58 @@ def render_free_cells(game_state) def draw_occupied_free_cells(game_state) game_state.free_cells.each do |card| with_border do - draw_card(card, game_state.selected_cards) + CardView.new(card).render(game_state.selected_cards) end end end def draw_empty_free_cells(game_state) - (4 - game_state.free_cells.count).times do - Curses.addstr('[ ]') - end + (4 - game_state.free_cells.count).times { Curses.addstr('[ ]') } end def render_foundations(game_state) %i[diamonds hearts spades clubs].each do |suit| - card = game_state.foundations[suit].last || EmptyCard.new - with_border do - draw_card(card, game_state.selected_cards) + card = game_state.foundations[suit].last + if card + with_border { CardView.new(card).render(game_state.selected_cards) } + else + Curses.addstr('[ ]') end end end - def render_cascades(game_state) + def with_border + Curses.addstr('[') + yield + Curses.addstr(']') + end + + def i_to_free_cell_letter(i) + %w[w x y z][i] + end + end + + # Renders the cascade section of the game + class CascadeView < BaseView + def render(game_state) + Curses.setpos(@y, 0) draw_cascade_cards(game_state) advance_y(by: 1) - Curses.setpos(@curr_y, 4) + Curses.setpos(@y, 4) game_state.cascades.length.times do |i| Curses.addstr("#{i_to_cascade_letter(i)} ") end + @y end + private + def draw_cascade_cards(game_state) printable_card_grid(game_state).each do |row| - Curses.addstr(' ') + Curses.setpos(@y, 3) row.each do |card| - draw_card(card, game_state.selected_cards) + CardView.new(card).render(game_state.selected_cards) if card + Curses.addstr(empty_card_string) if card.nil? Curses.addstr(' ') end advance_y(by: 1) @@ -155,89 +187,79 @@ def draw_cascade_cards(game_state) def printable_card_grid(game_state) max_length = game_state.cascades.map(&:length).max game_state.cascades.map do |c| - c + (0...max_length - c.count).map { EmptyCard.new } + c + (0...max_length - c.count).map { nil } end.transpose end - def i_to_free_cell_letter(i) - %w[w x y z][i] - end - def i_to_cascade_letter(i) [i + Freecell::CharacterParser::ASCII_LOWERCASE_A].pack('c*') end - def draw_card(card, selected_cards) - with_card_coloring(card, selected_cards) do |c| - str = case c - when Freecell::Card - card_string(c) - when Freecell::EmptyCard - empty_card_string - end - Curses.addstr(str) - end - end - - def card_string(card) - if card.rank < 10 - " #{card.rank}#{card.suit.to_s[0]}" - else - "#{card.rank}#{card.suit.to_s[0]}" - end - end - def empty_card_string ' ' end + end - def with_border - Curses.addstr('[') - yield - Curses.addstr(']') + # Bottom area of game + class BottomView < BaseView + def render(game_state) + draw_with_highlighted_first_letter(0, 'quit') + draw_with_highlighted_first_letter(5, 'undo') + draw_with_highlighted_first_letter(10, '?=help') + mvaddstr(@y, 28, "#{game_state.num_moves} moves,") + mvaddstr(@y, 37, "#{game_state.num_undos} undos") end - def black_card_color - @black_card_color ||= Curses.color_pair(BLACK_CARD_COLOR_PAIR_ID) - end + private - def black_selected_card_color - @black_selected_card_color ||= Curses.color_pair( - SELECTED_BLACK_CARD_COLOR_PAIR_ID - ) + def draw_with_highlighted_first_letter(x, str) + Curses.setpos(@y, x) + with_highlight_coloring { Curses.addstr(str[0]) } + Curses.addstr(str[1..-1]) end - def red_selected_card_color - @red_selected_card_color ||= Curses.color_pair( - SELECTED_RED_CARD_COLOR_PAIR_ID - ) + def with_highlight_coloring + Curses.attron(@colors.black_card_color) + yield + Curses.attroff(@colors.black_card_color) end + end - def with_card_coloring(card, selected_cards) - attr = color_for_card(card, selected_cards) - Curses.attron(attr) if attr - yield card - Curses.attroff(attr) if attr + # Commandline UI + class NCursesUI + def initialize + @curr_y = 0 + @input_sm = InputStateMachine.new end - def color_for_card(card, selected_cards) - attr = black_card_color if card.black? - return attr unless selected_cards && (card.red? || card.black?) - card_is_selected = selected_cards.include?(card) - attr = red_selected_card_color if card.red? && card_is_selected - attr = black_selected_card_color if card.black? && card_is_selected - attr + def setup + Curses.init_screen + Curses.cbreak + Curses.noecho + Curses.nonl + Curses.curs_set(0) + Colors.setup + ensure + Curses.close_screen end - def with_highlight_coloring - Curses.attron(black_card_color) - yield - Curses.attroff(black_card_color) + def render(game_state) + Curses.erase + TopView.new(0).render(game_state) + y = CascadeView.new(4).render(game_state) + BottomView.new(y + 1).render(game_state) + Curses.refresh end - def advance_y(by:) - @curr_y += by - Curses.setpos(@curr_y, 0) + def parse_input + command = @input_sm.handle_ch(Curses.getch) + return unless command + case command.type + when :quit + exit + else + command + end end end end