sams_tic_tac_toe 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|