sams_tic_tac_toe 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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