sams_tic_tac_toe 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +13 -0
- data/Rakefile +6 -0
- data/bin/play_tic_tac_toe +60 -0
- data/lib/tic_tac_toe/errors/invalid_selection.rb +5 -0
- data/lib/tic_tac_toe/model/board.rb +71 -0
- data/lib/tic_tac_toe/model/board_setup.rb +30 -0
- data/lib/tic_tac_toe/model/game_state.rb +114 -0
- data/lib/tic_tac_toe/model/game_tree.rb +72 -0
- data/lib/tic_tac_toe/model/move.rb +20 -0
- data/lib/tic_tac_toe/model/move_strategy.rb +86 -0
- data/lib/tic_tac_toe/model/piece.rb +15 -0
- data/lib/tic_tac_toe/model/team.rb +29 -0
- data/lib/tic_tac_toe/model/team_collection.rb +25 -0
- data/lib/tic_tac_toe/model/teams_setup.rb +37 -0
- data/lib/tic_tac_toe/model/tile.rb +17 -0
- data/lib/tic_tac_toe/model/tile_collection.rb +165 -0
- data/lib/tic_tac_toe/presenter/board.rb +51 -0
- data/lib/tic_tac_toe/presenter/select_team.rb +21 -0
- data/lib/tic_tac_toe/utils/terminal.rb +19 -0
- data/lib/tic_tac_toe/version.rb +3 -0
- data/lib/tic_tac_toe/view/abstract/base.rb +11 -0
- data/lib/tic_tac_toe/view/board.rb +49 -0
- data/lib/tic_tac_toe/view/game_result.rb +19 -0
- data/lib/tic_tac_toe/view/select_move.rb +39 -0
- data/lib/tic_tac_toe/view/select_team.rb +39 -0
- data/lib/tic_tac_toe.rb +7 -0
- data/tic_tac_toe.gemspec +37 -0
- metadata +163 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Model
|
2
|
+
class TeamCollection
|
3
|
+
def initialize(teams)
|
4
|
+
@teams = teams
|
5
|
+
@head = teams[0]
|
6
|
+
@rest = teams[1..-1]
|
7
|
+
end
|
8
|
+
|
9
|
+
def current
|
10
|
+
@head
|
11
|
+
end
|
12
|
+
|
13
|
+
def next
|
14
|
+
@rest << @head
|
15
|
+
|
16
|
+
@head = @rest.shift
|
17
|
+
end
|
18
|
+
|
19
|
+
def clone
|
20
|
+
teams = [@head].concat(@rest)
|
21
|
+
|
22
|
+
self.class.new(teams)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Model
|
2
|
+
class TeamsSetup
|
3
|
+
HUMAN_TYPE = 1
|
4
|
+
COMPUTER_TYPE = 2
|
5
|
+
TEAM_TYPES = [HUMAN_TYPE, COMPUTER_TYPE].freeze
|
6
|
+
|
7
|
+
attr_reader :team_types
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@team_klass = args[:team_klass]
|
11
|
+
@piece_klass = args[:piece_klass]
|
12
|
+
@move_klass = args[:move_klass]
|
13
|
+
@move_strategy_klass = args[:move_strategy_klass]
|
14
|
+
@team_types = { "Player": HUMAN_TYPE, "Computer": COMPUTER_TYPE }
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_teams(teams_args)
|
18
|
+
teams_args.map { |args| create_team(args) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid_team_type?(type)
|
22
|
+
TEAM_TYPES.include?(type)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def create_team(args)
|
28
|
+
name = args[:name]
|
29
|
+
piece = @piece_klass.new(name, @move_klass)
|
30
|
+
move_strategy = args[:type] == COMPUTER_TYPE ? @move_strategy_klass.new : nil
|
31
|
+
|
32
|
+
@team_klass.new(name: name,
|
33
|
+
move_strategy: move_strategy,
|
34
|
+
pieces: [piece])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# rubocop:disable Metrics/ClassLength
|
2
|
+
module Model
|
3
|
+
class TileCollection
|
4
|
+
attr_reader :dimensions
|
5
|
+
|
6
|
+
def initialize(tiles, dimensions)
|
7
|
+
raise ArgumentError, 'empty tiles array' if tiles.empty?
|
8
|
+
raise ArgumentError, 'dimensions do not match tiles length' if tiles.length != dimensions**2
|
9
|
+
|
10
|
+
@dimensions = dimensions
|
11
|
+
|
12
|
+
@tiles = tiles.each_with_index do |tile, i|
|
13
|
+
tile.col = col_index(i)
|
14
|
+
tile.row = row_index(i)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def id
|
19
|
+
@tiles.each_with_object('') do |tile, str|
|
20
|
+
str << tile.piece_name
|
21
|
+
str
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def each(&block)
|
26
|
+
@tiles.each(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def rows
|
30
|
+
return @rows unless @rows.nil?
|
31
|
+
|
32
|
+
i = 0
|
33
|
+
@rows = []
|
34
|
+
|
35
|
+
while i < @tiles.count
|
36
|
+
j = i + 3
|
37
|
+
|
38
|
+
@rows << @tiles[i...j]
|
39
|
+
|
40
|
+
i = j
|
41
|
+
end
|
42
|
+
|
43
|
+
@rows
|
44
|
+
end
|
45
|
+
|
46
|
+
def available_tiles
|
47
|
+
@tiles.find_all(&:available?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def available_tiles?
|
51
|
+
available_tiles.count.positive?
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_tile(row, col)
|
55
|
+
@tiles[index(row.to_i, col.to_i)]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Any given TicTacToe board has 8 equivalent boards, which are generated by flipping and rotating a board.
|
59
|
+
def equivalents
|
60
|
+
next_tile_collections = []
|
61
|
+
|
62
|
+
i = 4
|
63
|
+
next_tc = self
|
64
|
+
|
65
|
+
while i.positive?
|
66
|
+
rotated = next_tc.rotate
|
67
|
+
next_tile_collections << rotated
|
68
|
+
next_tile_collections << rotated.flip
|
69
|
+
|
70
|
+
next_tc = rotated
|
71
|
+
i -= 1
|
72
|
+
end
|
73
|
+
|
74
|
+
next_tile_collections
|
75
|
+
end
|
76
|
+
|
77
|
+
def clone(tiles = nil)
|
78
|
+
tiles ||= clone_tiles
|
79
|
+
|
80
|
+
self.class.new(tiles, dimensions)
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def rotate
|
86
|
+
tiles = clone_tiles
|
87
|
+
rotated = []
|
88
|
+
rotated_col_i = dimensions
|
89
|
+
|
90
|
+
(1..dimensions).each do |row_i|
|
91
|
+
(1..dimensions).each do |col_i|
|
92
|
+
rotate_tile(row_i, col_i, rotated_col_i, rotated, tiles)
|
93
|
+
end
|
94
|
+
|
95
|
+
rotated_col_i -= 1
|
96
|
+
end
|
97
|
+
|
98
|
+
clone(rotated)
|
99
|
+
end
|
100
|
+
|
101
|
+
def flip
|
102
|
+
tiles = clone_tiles
|
103
|
+
|
104
|
+
top_row_i = 1
|
105
|
+
bottom_row_i = dimensions
|
106
|
+
|
107
|
+
while top_row_i <= (dimensions / 2)
|
108
|
+
(1..dimensions).each do |col_i|
|
109
|
+
flip_tile(top_row_i, bottom_row_i, col_i, tiles)
|
110
|
+
end
|
111
|
+
|
112
|
+
top_row_i += 1
|
113
|
+
bottom_row_i -= 1
|
114
|
+
end
|
115
|
+
|
116
|
+
clone(tiles)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def flip_tile(top_row_i, bottom_row_i, col_i, tiles)
|
122
|
+
top_i = index(top_row_i, col_i)
|
123
|
+
bottom_i = index(bottom_row_i, col_i)
|
124
|
+
top = tiles[top_i]
|
125
|
+
bottom = tiles[bottom_i]
|
126
|
+
|
127
|
+
top.row = bottom_row_i
|
128
|
+
bottom.row = top_row_i
|
129
|
+
|
130
|
+
tiles[top_i] = bottom
|
131
|
+
tiles[bottom_i] = top
|
132
|
+
end
|
133
|
+
|
134
|
+
def rotate_tile(row_i, col_i, rotated_col_i, rotated, tiles)
|
135
|
+
tile_i = index(row_i, col_i)
|
136
|
+
rotated_tile_i = index(col_i, rotated_col_i)
|
137
|
+
tile = tiles[tile_i]
|
138
|
+
|
139
|
+
tile.row = col_i
|
140
|
+
tile.col = rotated_col_i
|
141
|
+
|
142
|
+
rotated[rotated_tile_i] = tile
|
143
|
+
end
|
144
|
+
|
145
|
+
def clone_tiles
|
146
|
+
@tiles.map(&:clone)
|
147
|
+
end
|
148
|
+
|
149
|
+
def index(row, col)
|
150
|
+
r = row - 1
|
151
|
+
c = col - 1
|
152
|
+
|
153
|
+
dimensions * r + c
|
154
|
+
end
|
155
|
+
|
156
|
+
def row_index(index)
|
157
|
+
(index / dimensions) + 1
|
158
|
+
end
|
159
|
+
|
160
|
+
def col_index(index)
|
161
|
+
(index % dimensions) + 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Presenter
|
2
|
+
class Board
|
3
|
+
def initialize(board, game_tree_klass)
|
4
|
+
@board = board
|
5
|
+
@game_tree_klass = game_tree_klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def select_move(row, col, team)
|
9
|
+
@board.set_piece(row, col, team.selected_piece)
|
10
|
+
|
11
|
+
@board.cycle_teams
|
12
|
+
end
|
13
|
+
|
14
|
+
def computer_select_move(team)
|
15
|
+
move_strategy = team.move_strategy
|
16
|
+
game_tree = @game_tree_klass.generate_game_tree(@board)
|
17
|
+
move = move_strategy.select_move(game_tree)
|
18
|
+
tile = move.tile
|
19
|
+
|
20
|
+
select_move(tile.row, tile.col, team)
|
21
|
+
end
|
22
|
+
|
23
|
+
def invalid_tile_selection?(row, col)
|
24
|
+
row > @board.dimensions || col > @board.dimensions || !@board.tile_available?(row, col)
|
25
|
+
end
|
26
|
+
|
27
|
+
def tile_collection
|
28
|
+
@board.tile_collection
|
29
|
+
end
|
30
|
+
|
31
|
+
def winning_team
|
32
|
+
@board.winner
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_team
|
36
|
+
@board.current_team
|
37
|
+
end
|
38
|
+
|
39
|
+
def winner?
|
40
|
+
!winning_team.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def continue?
|
44
|
+
!(draw? || winner?)
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw?
|
48
|
+
@board.complete? && !winner?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Presenter
|
2
|
+
class SelectTeam
|
3
|
+
def initialize(board_setup, teams_setup)
|
4
|
+
@board_setup = board_setup
|
5
|
+
@teams_setup = teams_setup
|
6
|
+
end
|
7
|
+
|
8
|
+
def invalid_team_selection?(type)
|
9
|
+
!@teams_setup.valid_team_type?(type)
|
10
|
+
end
|
11
|
+
|
12
|
+
def team_types
|
13
|
+
@teams_setup.team_types
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_teams(teams_args)
|
17
|
+
teams = @teams_setup.create_teams(teams_args)
|
18
|
+
@board_setup.teams = teams
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module View
|
2
|
+
class Board < View::Base
|
3
|
+
def initialize(board_presenter, table_klass)
|
4
|
+
@board_presenter = board_presenter
|
5
|
+
@table_klass = table_klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def render
|
9
|
+
tile_collection = @board_presenter.tile_collection
|
10
|
+
headings = generate_headings(tile_collection.dimensions)
|
11
|
+
rows = format_rows(tile_collection.rows)
|
12
|
+
table = @table_klass.new(headings: headings, rows: rows, style: { all_separators: true })
|
13
|
+
|
14
|
+
display_msg(table)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate_indexes(dimensions)
|
20
|
+
range = (1..dimensions)
|
21
|
+
|
22
|
+
range.each_with_object(['']) { |i, indexes| indexes << i }
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_headings(dimensions)
|
26
|
+
generate_indexes(dimensions)
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_rows(rows)
|
30
|
+
rows.each_with_object([]).with_index do |(row, arr), i|
|
31
|
+
formatted = format_row(row)
|
32
|
+
|
33
|
+
formatted.unshift(i + 1)
|
34
|
+
|
35
|
+
arr << formatted
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_row(row)
|
40
|
+
formatted = row.map do |tile|
|
41
|
+
piece = tile.piece
|
42
|
+
|
43
|
+
piece.nil? ? '' : piece.name
|
44
|
+
end
|
45
|
+
|
46
|
+
formatted
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module View
|
2
|
+
class GameResult < View::Base
|
3
|
+
DRAW_MESSAGE = 'Draw!'.freeze
|
4
|
+
|
5
|
+
def initialize(board_presenter)
|
6
|
+
@board_presenter = board_presenter
|
7
|
+
end
|
8
|
+
|
9
|
+
def render
|
10
|
+
if @board_presenter.draw?
|
11
|
+
display_msg(DRAW_MESSAGE)
|
12
|
+
elsif @board_presenter.winner?
|
13
|
+
winning_team = @board_presenter.winning_team
|
14
|
+
|
15
|
+
display_msg("Team #{winning_team.name} Won!!!")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module View
|
2
|
+
class SelectMove < View::Base
|
3
|
+
SELECT_ROW_MESSAGE = 'Please select a row'.freeze
|
4
|
+
SELECT_COL_MESSAGE = 'Please select a col'.freeze
|
5
|
+
|
6
|
+
def initialize(board_presenter, terminal_util)
|
7
|
+
@board_presenter = board_presenter
|
8
|
+
@terminal_util = terminal_util
|
9
|
+
end
|
10
|
+
|
11
|
+
def render
|
12
|
+
display_msg("Go #{@board_presenter.current_team.name}")
|
13
|
+
|
14
|
+
select_move
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def select_move
|
20
|
+
current_team = @board_presenter.current_team
|
21
|
+
|
22
|
+
if current_team.computer?
|
23
|
+
@board_presenter.computer_select_move(current_team)
|
24
|
+
else
|
25
|
+
display_msg(SELECT_ROW_MESSAGE)
|
26
|
+
|
27
|
+
row = @terminal_util.get_integer_input
|
28
|
+
|
29
|
+
display_msg(SELECT_COL_MESSAGE)
|
30
|
+
|
31
|
+
col = @terminal_util.get_integer_input
|
32
|
+
|
33
|
+
raise InvalidSelection, 'Invalid Tile Selection :(' if @board_presenter.invalid_tile_selection?(row, col)
|
34
|
+
|
35
|
+
@board_presenter.select_move(row, col, current_team)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module View
|
2
|
+
class SelectTeam < View::Base
|
3
|
+
SELECT_TEAM_TYPE_MESSAGE = 'Please Select Team Type by Entering Number Next to Type'.freeze
|
4
|
+
SELECT_TEAM_NAME_MESSAGE = 'Please Select Team Name'.freeze
|
5
|
+
|
6
|
+
def initialize(select_team_presenter, terminal_util)
|
7
|
+
@select_team_presenter = select_team_presenter
|
8
|
+
@terminal_util = terminal_util
|
9
|
+
end
|
10
|
+
|
11
|
+
def render
|
12
|
+
select_teams
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def select_teams
|
18
|
+
teams = (1..2).map { |_| select_team(@select_team_presenter.team_types) }
|
19
|
+
|
20
|
+
@select_team_presenter.set_teams(teams)
|
21
|
+
end
|
22
|
+
|
23
|
+
def select_team(team_types)
|
24
|
+
display_msg(SELECT_TEAM_TYPE_MESSAGE)
|
25
|
+
|
26
|
+
team_types.each { |k, v| display_msg("#{v}: #{k}") }
|
27
|
+
|
28
|
+
type = @terminal_util.get_integer_input
|
29
|
+
|
30
|
+
raise InvalidSelection, 'Invalid Team Selection :(' if @select_team_presenter.invalid_team_selection?(type)
|
31
|
+
|
32
|
+
display_msg(SELECT_TEAM_NAME_MESSAGE)
|
33
|
+
|
34
|
+
name = @terminal_util.get_input
|
35
|
+
|
36
|
+
{ type: type, name: name }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/tic_tac_toe.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'terminal-table'
|
2
|
+
|
3
|
+
Dir[File.join(__dir__, 'tic_tac_toe', 'view', 'abstract', '*.rb')].sort.each { |file| require file }
|
4
|
+
|
5
|
+
Dir[File.join(__dir__, '..', 'spec', 'factories', '*.rb')].sort.each { |file| require file }
|
6
|
+
|
7
|
+
Dir[File.join(__dir__, '**', '*.rb')].sort.each { |file| require file }
|
data/tic_tac_toe.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'tic_tac_toe/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'sams_tic_tac_toe'
|
7
|
+
spec.version = TicTacToe::VERSION
|
8
|
+
spec.authors = ['Sam Eckmeier']
|
9
|
+
spec.email = ['eckmeier41@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Simple, command line tic tac toe game'
|
12
|
+
spec.description = ''
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
16
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
17
|
+
if spec.respond_to?(:metadata)
|
18
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
19
|
+
else
|
20
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
21
|
+
'public gem pushes.'
|
22
|
+
end
|
23
|
+
|
24
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
f.match(%r{^(test|spec|features)/})
|
26
|
+
end
|
27
|
+
spec.bindir = 'bin'
|
28
|
+
spec.executables = ['play_tic_tac_toe']
|
29
|
+
spec.require_paths = ['lib']
|
30
|
+
|
31
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
32
|
+
spec.add_development_dependency 'factory_bot', '~> 5.1.1'
|
33
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
34
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
35
|
+
spec.add_development_dependency 'rubocop', '~> 0.80.1'
|
36
|
+
spec.add_runtime_dependency 'terminal-table', '~> 1.8'
|
37
|
+
end
|