Class: Upwords::UI

Inherits:
Object
  • Object
show all
Defined in:
lib/upwords/ui.rb

Instance Method Summary collapse

Constructor Details

#initialize(game, row_height = 1, col_width = 4) ⇒ UI

Returns a new instance of UI.



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/upwords/ui.rb', line 4

def initialize(game, row_height = 1, col_width = 4)
  # Game and drawing variables
  @game = game
  @rows = game.board.num_rows
  @cols = game.board.num_columns
  @row_height = row_height
  @col_width = col_width
  @rack_visible = false

  # Configure Curses and initialize screen
  Curses.noecho
  Curses.curs_set(2)  # Blinking cursor
  Curses.init_screen
  Curses.start_color

  # Initialize colors
  Curses.init_pair(RED, Curses::COLOR_RED, Curses::COLOR_BLACK) # Red on black background
  Curses.init_pair(YELLOW, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # Yellow on black background

  # Initialize main window and game loop
  begin
    @win = Curses.stdscr
    @win.keypad=(true)

    add_players
    @game.all_refill_racks

    @win.setpos(*letter_pos(*@game.cursor.pos))
    draw_update_loop
  ensure
    Curses.close_screen
  end      
end

Instance Method Details

#add_playersObject

Select the maximum number of players, then add players and select if they humans or computers TODO: refactor this method



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/upwords/ui.rb', line 135

def add_players
  @win.setpos(0, 0)
  Curses.echo
  @win.keypad=(false) 
  
  num_players = 0
  
  # Select how many players will be in the game
  # TODO: Add a command-line flag to allow players to skip this step
  until (1..@game.max_players).include?(num_players.to_i) do
    @win.addstr("How many players will play? (1-#{@game.max_players})\n")
    num_players = @win.getstr
    
    @win.addstr("Invalid selection\n") if !(1..@game.max_players).include?(num_players.to_i)
    @win.addstr("\n")

    # Refresh screen if lines go beyond terminal window   
    clear_terminal if @win.cury >= @win.maxy - 1
  end

  # Name each player and choose if they are humans or computers
  # TODO: Add a command-line flag to set this
  (1..num_players.to_i).each do |idx|
    @win.addstr("What is Player #{idx}'s name? (Press enter to submit...)\n")

    name = @win.getstr
    name = (name =~ /[[:alpha:]]/ ? name : sprintf('Player %d', idx))
    @win.addstr("\nIs #{name} a computer? (y/n)\n")
    
    cpu = @win.getstr
    @game.add_player(name, rack_capacity=7, cpu.upcase == "Y")
    @win.addstr("\n")        

    # Refresh screen if lines go beyond terminal window
    clear_terminal if @win.cury >= @win.maxy - 1
  end
ensure
  Curses.noecho
  @win.keypad=(true) 
end

#clear_messageObject



199
200
201
# File 'lib/upwords/ui.rb', line 199

def clear_message
  draw_message("")
end

#draw_confirm(text) ⇒ Object

TODO: make confirmation options more clear



218
219
220
221
222
223
# File 'lib/upwords/ui.rb', line 218

def draw_confirm(text) 
  draw_message("#{text}")
  reply = (@win.getch.to_s).upcase == "Y"
  clear_message
  return reply
end

#draw_controlsObject



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/upwords/ui.rb', line 238

def draw_controls
  draw_wrapper do
    y, x = controls_info_pos
    ["----------------------",
     "|      Controls      |",
     "----------------------",
     "Show Letters   [SPACE]",
     "Undo Last Move [DEL]",
     "Submit Move    [ENTER]",
     "Swap Letter    [+]",
     "Skip Turn      [-]",
     "Quit Game      [SHIFT+Q]",
     "Force Quit     [CTRL+Z]"      # TODO: technically this only works for unix shells...
    ].each_with_index do |line, i|
      @win.setpos(y+i, x)
      @win.addstr(line)
    end
  end
end

#draw_gridObject



225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/upwords/ui.rb', line 225

def draw_grid
  draw_wrapper do
    # create a list containing each line of the board string
    divider = [nil, ["-" * @col_width] * @cols, nil].flatten.join("+")
    spaces = [nil, [" " * @col_width] * @cols, nil].flatten.join("|")
    lines = ([divider] * (@rows + 1)).zip([spaces] * @rows).flatten

    # concatenate board lines and draw in a sub-window on the terminal
    @win.setpos(0, 0)
    @win.addstr(lines.join("\n"))
  end
end

#draw_lettersObject



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/upwords/ui.rb', line 258

def draw_letters
  board = @game.board

  draw_for_each_cell do |row, col|
    @win.setpos(*letter_pos(row, col))
    
    # HACK: removes yellow highlighting from 'Qu'
    letter = "#{board.top_letter(row, col)}  "[0..1] 

    if @game.pending_position?(row, col)
      Curses.attron(Curses.color_pair(YELLOW)) { @win.addstr(letter) }
    else
      @win.addstr(letter)      
    end  
  end
end

#draw_message(text) ⇒ Object

Draw individual game elements



195
196
197
# File 'lib/upwords/ui.rb', line 195

def draw_message(text)
  write_str(*message_pos, text, clear_below=true)
end

#draw_player_infoObject



203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/upwords/ui.rb', line 203

def draw_player_info
  draw_wrapper do
    py, px = player_info_pos
    
    # Draw rack for current player only          
    write_str(py, px, "#{@game.current_player.name}'s letters:", clear_right=true)
    write_str(py+1, px, "[#{@game.current_player.show_rack(masked=!@rack_visible)}]", clear_right=true)

    @game.players.each_with_index do |p, i|
      write_str(py+i+3, px, sprintf("%s %-13s %4d", p == @game.current_player ? "->" : "  ", "#{p.name}:", p.score), clear_right=true)
    end
  end
end

#draw_stack_heightsObject



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/upwords/ui.rb', line 275

def draw_stack_heights
  board = @game.board

  draw_for_each_cell do |row, col|        
    @win.setpos(*stack_height_pos(row, col))

    case (height = board.stack_height(row, col))
    when 0
      @win.addstr("-")
    when board.max_height
      Curses.attron(Curses.color_pair(RED)) { @win.addstr(height.to_s) }
    else
      @win.addstr(height.to_s)
    end
  end
end

#draw_update_loopObject

Main methods: draw and input loops



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/upwords/ui.rb', line 42

def draw_update_loop
  draw_grid
  draw_controls

  while @game.running? do
    @rack_visible = false
    draw_player_info
    draw_message "#{@game.current_player.name}'s turn"

    # CPU move subroutine
    if @game.current_player.cpu?
      draw_message "#{@game.current_player.name} is thinking..."
      @game.cpu_move
      draw_letters
      draw_stack_heights
      draw_player_info 
    # Read key inputs then update cursor and window
    else
      while read_key do
        @win.setpos(*letter_pos(*@game.cursor.pos))
        draw_letters
        draw_stack_heights
        draw_player_info 
      end
    end
    
    # Game over subroutine
    if @game.game_over?
      draw_player_info 
      get_game_result
    end

  end
end

#get_game_resultObject



176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/upwords/ui.rb', line 176

def get_game_result
  draw_confirm("The game is over. Press any key to continue to see who won...")
  
  top_score = @game.get_top_score
  winners = @game.get_winners
  
  if winners.size == 1 
    draw_confirm "And the winner is... #{winners.first} with #{top_score} points!"
  else
    draw_confirm "We have a tie! #{winners.join(', ')} all win with #{top_score} points!"
  end
  
  @game.exit_game
end

#read_keyObject

If read_key returns ‘false’, then current iteration of the input loop ends



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/upwords/ui.rb', line 78

def read_key
  case (key = @win.getch)      
  when 'Q'
    if draw_confirm("Are you sure you want to exit the game? (y/n)")
      @game.exit_game 
      return false
    end
  when DELETE
    @game.undo_last
    draw_message(@game.standard_message) # TODO: factor this method
  when Curses::Key::UP
    @game.cursor.up
  when Curses::Key::DOWN
    @game.cursor.down
  when Curses::Key::LEFT
    @game.cursor.left
  when Curses::Key::RIGHT
    @game.cursor.right
  when SPACE
    @rack_visible = !@rack_visible
  when ENTER
    if draw_confirm("Are you sure you wanted to submit? (y/n)")  
      @game.submit_moves
      return false
    end
  when '+'
    draw_message("Pick a letter to swap")
    letter = @win.getch
    if letter =~ /[[:alpha:]]/ && draw_confirm("Swap '#{letter}' for a new letter? (y/n)")
      @game.swap_letter(letter)
      return false
    else
      draw_message("'#{letter}' is not a valid letter")
    end
  when '-'
    if draw_confirm("Are you sure you wanted to skip your turn? (y/n)")  
      @game.skip_turn
      return false
    end
  when /[[:alpha:]]/
    @game.play_letter(key)
    draw_message(@game.standard_message)
  end
  
  return true

rescue IllegalMove => exception
  draw_confirm("#{exception.message} (press any key to continue...)")
  return true 
end