Class: TicTacToe::Game

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

Constant Summary collapse

NotPlayable =
Class.new(StandardError)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(size, symbols, player_names) ⇒ Game

Returns a new instance of Game.

Raises:



41
42
43
44
45
46
47
48
49
50
# File 'lib/tic_tac_toe.rb', line 41

def initialize size, symbols, player_names
  @symbols = eval(symbols || "") || ['x', 'o']
  @names = eval(player_names || "") || []
  @size = (size || 3).to_i

  raise NotPlayable,
    "Game is not playable. See examples of possible arguments in source code!"\
    unless playable?
  play
end

Instance Attribute Details

#current_mark_posObject

Returns the value of attribute current_mark_pos.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def current_mark_pos
  @current_mark_pos
end

#current_playerObject

Returns the value of attribute current_player.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def current_player
  @current_player
end

#game_overObject

Returns the value of attribute game_over.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def game_over
  @game_over
end

#namesObject (readonly)

Returns the value of attribute names.



39
40
41
# File 'lib/tic_tac_toe.rb', line 39

def names
  @names
end

#play_tableObject

Returns the value of attribute play_table.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def play_table
  @play_table
end

#playersObject

Returns the value of attribute players.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def players
  @players
end

#sizeObject (readonly)

Returns the value of attribute size.



39
40
41
# File 'lib/tic_tac_toe.rb', line 39

def size
  @size
end

#symbolsObject (readonly)

Returns the value of attribute symbols.



39
40
41
# File 'lib/tic_tac_toe.rb', line 39

def symbols
  @symbols
end

#unique_columnsObject

Returns the value of attribute unique_columns.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def unique_columns
  @unique_columns
end

#unique_diagsObject

Returns the value of attribute unique_diags.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def unique_diags
  @unique_diags
end

#unique_rowsObject

Returns the value of attribute unique_rows.



36
37
38
# File 'lib/tic_tac_toe.rb', line 36

def unique_rows
  @unique_rows
end

Instance Method Details

#boldize(text) ⇒ Object



291
292
293
294
295
# File 'lib/tic_tac_toe.rb', line 291

def boldize text
  style text do
    1
  end
end

#colorize(text) ⇒ Object



279
280
281
282
283
# File 'lib/tic_tac_toe.rb', line 279

def colorize text
  style text do
    current_player.color
  end
end

#dim(text) ⇒ Object



297
298
299
300
301
# File 'lib/tic_tac_toe.rb', line 297

def dim text
  style text do
    2
  end
end

#end_game(symbol = nil) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/tic_tac_toe.rb', line 205

def end_game symbol = nil
  self.game_over = true
  refresh_screen
  show_play_table
  puts
  print "Game Over. "
  if symbol == :draw
    puts "It's a draw."
  else
    current_player.wins
  end
end

#flash(text) ⇒ Object



313
314
315
316
317
318
319
320
321
322
323
# File 'lib/tic_tac_toe.rb', line 313

def flash text
  print "\r\e[A" + " " * term_width
  prompt
  if STYLE == :off
    print text
  else
    print "\e[33m#{text}\e[0m"
  end
  sleep 2
  puts
end

#forced_to_quitObject



271
272
273
274
275
276
277
# File 'lib/tic_tac_toe.rb', line 271

def forced_to_quit
  puts
  refresh_screen
  print "Game Over. "
  puts italicize("Forced to quit.")
  exit
end

#init_playersObject



58
59
60
61
62
63
64
65
# File 'lib/tic_tac_toe.rb', line 58

def init_players
  self.players = []
  symbols.compact.uniq.size.times do
    name = self.names.shift
    symbol = self.symbols.shift
    self.players << Player.new({name: name, symbol: symbol})
  end
end

#italicize(text) ⇒ Object



285
286
287
288
289
# File 'lib/tic_tac_toe.rb', line 285

def italicize text
  style text do
    3
  end
end

#no_longer_winnable?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/tic_tac_toe.rb', line 92

def no_longer_winnable?
  (unique_rows + unique_columns + unique_diags).empty?
end

#playObject



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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/tic_tac_toe.rb', line 145

def play
  init_players
  player = self.players.cycle.each

  self.play_table = Array.new(@size){ Array.new(@size) }
  self.unique_rows = [*(0...size)]
  self.unique_columns = [*(0...size)]
  self.unique_diags = [0, 1]

  self.game_over = false
  prepare_screen
  until game_over do
    self.current_player = player.next

    pos_marked = false
    until pos_marked do
      refresh_screen
      show_play_table
      show_note
      prompt
      # Get input
      begin
        input = gets
        if input.nil? # Ctrl + D pressed
          raise Interrupt
        else
          typed_text = input.chomp.strip
        end
      rescue SystemExit, Interrupt # Ctrl + C pressed or program/ruby failure
        forced_to_quit
      end

      wrong_input = wrong_input? typed_text
      if wrong_input
        flash wrong_input
        next
      end

      # The row and column is 1 based
      self.current_mark_pos = typed_text.split(/,\s*/).map{|c| c.to_i-1}

      mark_row, mark_col = *self.current_mark_pos
      symbol = self.current_player.symbol

      unless play_table[mark_row][mark_col]
        play_table[mark_row][mark_col] = symbol
        pos_marked = true
        run_some_checks
      else
        flash "Already marked. Choose another position!"
      end
    end
  end
end

#playable?Boolean

Returns:

  • (Boolean)


67
68
69
70
71
72
73
74
75
76
# File 'lib/tic_tac_toe.rb', line 67

def playable?
  size > 2 && size <= 50 &&
    symbols.compact.uniq.size > 1 &&
    names.compact.uniq.size <= symbols.compact.uniq.size &&
    symbols.compact.uniq.all? do |m|
      m.is_a?(String) &&        # each should be string
        m.chars.count == 1 &&   # each should contain 1 character
        m.bytes.count == 1      # each should occupy 1 byte
    end
end

#prepare_screenObject



259
260
261
# File 'lib/tic_tac_toe.rb', line 259

def prepare_screen
  (size+5).times{ puts }
end

#promptObject



230
231
232
# File 'lib/tic_tac_toe.rb', line 230

def prompt
  print "\r #{colorize(current_player.name)}: "
end

#refresh_screenObject



263
264
265
266
267
268
269
# File 'lib/tic_tac_toe.rb', line 263

def refresh_screen
  print "\r\e[#{size+5}A"
  (size+5).times do
    print " " * term_width + "\n"
  end
  print "\r\e[#{size+5}A"
end

#run_some_checksObject



200
201
202
203
# File 'lib/tic_tac_toe.rb', line 200

def run_some_checks
  end_game if solved?
  end_game :draw if no_longer_winnable?
end

#show_noteObject

Screen related methods



225
226
227
228
# File 'lib/tic_tac_toe.rb', line 225

def show_note
  puts dim("Type: <#{italicize("row")}>\,<#{italicize("column")}>")
  puts
end

#show_play_tableObject



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/tic_tac_toe.rb', line 234

def show_play_table
  size_width = size.to_s.length
  col_label = "COLUMN".center(3*size)
  row_label = "ROW".center(size)
  print " "*(size_width + 4)
  print "#{italicize(col_label)}" + "\n"
  print " "*(size_width + 4)
  1.upto(size) do |cnum|
    print "#{boldize(cnum.to_s.center(3))}"
  end
  print "\n"
  play_table.each.with_index(1) do |row, rnum|
    print " #{italicize(row_label[rnum-1])}"
    print " #{boldize(rnum.to_s.rjust(size_width))} "
    row.each do |el|
      if STYLE == :off
        print "[#{el || " "}]"
      else
        print el ? "[#{el || " "}]\e[0m" : "[#{el || " "}]"
      end
    end
    puts
  end
end

#solved?Boolean

This method will run faster every ‘play` loop due to the decreasing number of `unique_<lines>` members

Returns:

  • (Boolean)


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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/tic_tac_toe.rb', line 98

def solved?
  mark_row, mark_col = *current_mark_pos
  symbol = current_player.symbol

  # Check if solved horizontally
  if unique_rows.any? and unique_rows.include? mark_row
    row = play_table[mark_row]
    return true if row.all?{|m| m == symbol }

    # Remove row from unique list if no longer unique
    unique_rows.delete(mark_row) if row.compact.uniq.size > 1
  end

  # Check if solved vertically
  if unique_columns.any? and unique_columns.include? mark_col
    column = []
    size.times do |i|
      column << play_table[i][mark_col]
    end
    return true if column.all?{|m| m == symbol }

    # Remove column unique list if no longer unique
    unique_columns.delete(mark_col) if column.compact.uniq.size > 1
  end

  # Check if solved diagonally
  if (mark_row == mark_col || mark_row == size - mark_col - 1) &&
        unique_diags.any?
    # Initialize diagonals with negative and positive gradients
    neg_diag = []
    pos_diag = []

    size.times do |i|
      neg_diag << play_table[i][i]
      pos_diag << play_table[size-i-1][i]
    end
    return true if neg_diag.all?{|m| m == symbol } || pos_diag.all?{|m| m == symbol }

    # Remove diagonal if no longer unique
    unique_diags.delete(0) if neg_diag.compact.uniq.size > 1
    unique_diags.delete(1) if pos_diag.compact.uniq.size > 1
  end

  # Not yet solved
  return false
end

#style(text) ⇒ Object



303
304
305
306
307
308
309
310
311
# File 'lib/tic_tac_toe.rb', line 303

def style text
  unless STYLE == :off
    text = text.gsub(/\e\[0m/,"")
    closing = "\e[0m"
    opening_code = yield
    text = "\e[#{opening_code}m#{text}#{closing}"
  end
  return text
end

#term_widthObject



218
219
220
# File 'lib/tic_tac_toe.rb', line 218

def term_width
  @term_width ||= `tput cols`.to_i
end

#wrong_input?(input) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/tic_tac_toe.rb', line 78

def wrong_input? input
  message = ""
  if m = input.match(/^(\d+),\s*(\d+)$/)
    if m.to_a.all?{|el| el.to_i.between?(1,size) }
      return false
    else
      message = "Input number should be between 1 and #{size}."
    end
  else
    message = "Make sure input is in the following format: <number>,<number>"
  end
  return message
end