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.
@@ -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,17 @@
1
+ module Model
2
+ class Tile
3
+ attr_accessor :piece, :row, :col
4
+
5
+ def available?
6
+ piece.nil?
7
+ end
8
+
9
+ def team
10
+ piece&.team
11
+ end
12
+
13
+ def piece_name
14
+ piece ? piece.name : '-'
15
+ end
16
+ end
17
+ 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,19 @@
1
+ module Utils
2
+ class Terminal
3
+ class << self
4
+ def clear_screen
5
+ system('clear') || system('cls')
6
+ end
7
+
8
+ def get_input
9
+ val = gets.chomp
10
+
11
+ val
12
+ end
13
+
14
+ def get_integer_input
15
+ get_input.to_i
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module TicTacToe
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,11 @@
1
+ module View
2
+ class Base
3
+ def render
4
+ raise NotImplementedError
5
+ end
6
+
7
+ def display_msg(msg)
8
+ puts msg
9
+ end
10
+ end
11
+ 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
@@ -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 }
@@ -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