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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 69fa18fbe3db990208b96d2c4548172003183e22fb33ee68f7e62c876673c39d
4
+ data.tar.gz: ae92cce1b2ba79b792b193adcc0cc0a48100e7430ceb4cdeca94501d3ded2aa3
5
+ SHA512:
6
+ metadata.gz: 48a41718853b5ccaa35803208f9b09d17c2cb52d7f9c5efaf6a2a1aa0adafd21c1d2f0281cf5fc71636aebaa34b8e64f0d8fd7d1013bf0712f998e2a9ac3df40
7
+ data.tar.gz: 53bc39b7b03115dddb57650bb76a4df0274ead5b699004b2bc8d29a9fdead4cd1ea9282ffb2a1f761c8cc70e97475aab0a99eab2e8f894558f6d009a36820afe
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .DS_Store
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
1
+ Layout/LineLength:
2
+ Max: 120
3
+
4
+ Style/Documentation:
5
+ Enabled: false
6
+
7
+ Naming/AccessorMethodName:
8
+ Enabled: false
9
+
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - 'spec/**/*'
13
+
14
+ Metrics/MethodLength:
15
+ Max: 20
16
+
17
+ Style/FrozenStringLiteralComment:
18
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.14.6
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at eckmeier41@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tic_tac_toe.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Sam Eckmeier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # TicTacToe
2
+
3
+ ## Description
4
+ Welcome to a game of command-line, tic-tac-toe! It's important to note that the computer is extremely
5
+ difficult to beat. If you do, then you might be a computer. Have fun!
6
+
7
+ ## Installation
8
+ There are two ways to play this game:
9
+ - Download this code here, through github, execute `bundle install` from the root dir, and finally run `bin/play_tic_tac_toe`
10
+ - Run `gem install sams_tic_tac_toe` and execute `play_tic_tac_toe`
11
+
12
+ If you decide to download this as a gem, make sure that you have RubyGems installed by running `gem --version` in your terminal.
13
+ If it isn't installed, refer to https://github.com/rubygems/rubygems.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'irb'
5
+ require File.expand_path("#{File.dirname(__FILE__)}/../lib/tic_tac_toe.rb")
6
+
7
+ begin
8
+ board_setup = Model::BoardSetup.new(
9
+ dimensions: 3,
10
+ board_klass: Model::Board,
11
+ tile_collection_klass: Model::TileCollection,
12
+ team_collection_klass: Model::TeamCollection,
13
+ game_state: Model::GameState,
14
+ tile_klass: Model::Tile
15
+ )
16
+ teams_setup = Model::TeamsSetup.new(
17
+ team_klass: Model::Team,
18
+ piece_klass: Model::Piece,
19
+ move_klass: Model::Move,
20
+ move_strategy_klass: Model::MoveStrategy
21
+ )
22
+ select_team_presenter = Presenter::SelectTeam.new(board_setup, teams_setup)
23
+ select_team_view = View::SelectTeam.new(select_team_presenter, Utils::Terminal)
24
+
25
+ begin
26
+ select_team_view.render
27
+ rescue InvalidSelection => e
28
+ puts e.message
29
+ puts 'Please Try Again :)'
30
+ retry
31
+ end
32
+
33
+ board = board_setup.create_board
34
+ board_presenter = Presenter::Board.new(board, Model::GameTree)
35
+ board_view = View::Board.new(board_presenter, Terminal::Table)
36
+ game_result_view = View::GameResult.new(board_presenter)
37
+ select_move_view = View::SelectMove.new(board_presenter, Utils::Terminal)
38
+
39
+ while board_presenter.continue?
40
+ Utils::Terminal.clear_screen
41
+
42
+ board_view.render
43
+
44
+ begin
45
+ select_move_view.render
46
+ rescue InvalidSelection => e
47
+ puts e.message
48
+ puts 'Please Try Again :)'
49
+ retry
50
+ end
51
+ end
52
+
53
+ Utils::Terminal.clear_screen
54
+
55
+ board_view.render
56
+
57
+ game_result_view.render
58
+ rescue StandardError
59
+ puts 'Seems that something went wrong :( Sorry about that'
60
+ end
@@ -0,0 +1,5 @@
1
+ class InvalidSelection < StandardError
2
+ def initialize(msg)
3
+ super(msg)
4
+ end
5
+ end
@@ -0,0 +1,71 @@
1
+ module Model
2
+ class Board
3
+ attr_reader :tile_collection
4
+
5
+ def initialize(args)
6
+ @tile_collection = args[:tile_collection]
7
+ @team_collection = args[:team_collection]
8
+ @game_state = args[:game_state]
9
+ end
10
+
11
+ def dimensions
12
+ @tile_collection.dimensions
13
+ end
14
+
15
+ def set_piece(row, col, piece)
16
+ tile = @tile_collection.find_tile(row, col)
17
+
18
+ tile.piece = piece
19
+
20
+ tile
21
+ end
22
+
23
+ def tile_available?(row, col)
24
+ tile = @tile_collection.find_tile(row, col)
25
+
26
+ tile.piece.nil?
27
+ end
28
+
29
+ def available_moves
30
+ current_team.available_moves(self)
31
+ end
32
+
33
+ def available_tiles
34
+ @tile_collection.available_tiles
35
+ end
36
+
37
+ def complete?
38
+ available_tiles.count.zero? || !winner.nil?
39
+ end
40
+
41
+ def current_team
42
+ @team_collection.current
43
+ end
44
+
45
+ def cycle_teams
46
+ @team_collection.next
47
+ end
48
+
49
+ def winner
50
+ @game_state.winner(self)
51
+ end
52
+
53
+ def rating(team)
54
+ rating = @game_state.rating(self, team)
55
+
56
+ if rating.negative?
57
+ rating -= available_tiles.count
58
+ elsif rating.positive?
59
+ rating += available_tiles.count
60
+ end
61
+
62
+ rating
63
+ end
64
+
65
+ def clone
66
+ self.class.new(tile_collection: @tile_collection.clone,
67
+ team_collection: @team_collection.clone,
68
+ game_state: @game_state.clone)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ module Model
2
+ class BoardSetup
3
+ attr_accessor :teams
4
+
5
+ def initialize(args)
6
+ @board_klass = args[:board_klass]
7
+ @tile_collection_klass = args[:tile_collection_klass]
8
+ @team_collection_klass = args[:team_collection_klass]
9
+ @game_state = args[:game_state]
10
+ @tile_klass = args[:tile_klass]
11
+ @dimensions = args[:dimensions]
12
+ @teams = []
13
+ end
14
+
15
+ def create_board
16
+ tiles = create_tiles
17
+
18
+ @board_klass.new(tile_collection: @tile_collection_klass.new(tiles, @dimensions),
19
+ team_collection: @team_collection_klass.new(@teams),
20
+ game_state: @game_state.new)
21
+ end
22
+
23
+ private
24
+
25
+ def create_tiles
26
+ cnt = @dimensions**2
27
+ (1..cnt).map { |_i| @tile_klass.new }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,114 @@
1
+ module Model
2
+ class GameState
3
+ def winner(board)
4
+ tile_collection = board.tile_collection
5
+
6
+ rows(tile_collection) || cols(tile_collection) || diags(tile_collection)
7
+ end
8
+
9
+ def rating(board, team)
10
+ winner = winner(board)
11
+
12
+ return 0 unless winner
13
+
14
+ winner.name == team.name ? 1 : -1
15
+ end
16
+
17
+ private
18
+
19
+ def current_team?(current_team, team)
20
+ (team && current_team) && team.name == current_team.name
21
+ end
22
+
23
+ def team(row_i, col_i, tile_collection)
24
+ tile = tile_collection.find_tile(row_i, col_i)
25
+
26
+ tile.team
27
+ end
28
+
29
+ def rows(tile_collection)
30
+ dims = tile_collection.dimensions
31
+
32
+ (1..dims).each do |row_i|
33
+ current_team = team(row_i, 1, tile_collection)
34
+
35
+ (2..dims).each do |col_i|
36
+ team = team(row_i, col_i, tile_collection)
37
+
38
+ unless current_team?(current_team, team)
39
+ current_team = nil
40
+ break
41
+ end
42
+ end
43
+
44
+ return current_team if current_team
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ def cols(tile_collection)
51
+ dims = tile_collection.dimensions
52
+
53
+ (1..dims).each do |col_i|
54
+ current_team = team(1, col_i, tile_collection)
55
+
56
+ (2..dims).each do |row_i|
57
+ team = team(row_i, col_i, tile_collection)
58
+
59
+ unless current_team?(current_team, team)
60
+ current_team = nil
61
+ break
62
+ end
63
+ end
64
+
65
+ return current_team if current_team
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ def left_diag(tile_collection)
72
+ current_team = team(1, 1, tile_collection)
73
+ dims = tile_collection.dimensions
74
+
75
+ (2..dims).each do |i|
76
+ team = team(i, i, tile_collection)
77
+
78
+ unless current_team?(current_team, team)
79
+ current_team = nil
80
+ break
81
+ end
82
+ end
83
+
84
+ current_team
85
+ end
86
+
87
+ def right_diag(tile_collection)
88
+ dims = tile_collection.dimensions
89
+ col_i = dims
90
+ current_team = team(1, col_i, tile_collection)
91
+
92
+ (2..dims).each do |row_i|
93
+ col_i -= 1
94
+
95
+ team = team(row_i, col_i, tile_collection)
96
+
97
+ unless current_team?(current_team, team)
98
+ current_team = nil
99
+ break
100
+ end
101
+ end
102
+
103
+ current_team
104
+ end
105
+
106
+ def diags(tile_collection)
107
+ team = left_diag(tile_collection)
108
+
109
+ return team if team
110
+
111
+ right_diag(tile_collection)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,72 @@
1
+ # GameTree is an n-ary tree and produces its children dynamically.
2
+
3
+ module Model
4
+ class GameTree
5
+ attr_reader :previous_move, :board
6
+
7
+ def initialize(board, previous_move = nil)
8
+ @board = board
9
+ @previous_move = previous_move
10
+ @equivalent = {}
11
+ end
12
+
13
+ class << self
14
+ def generate_game_tree(board, previous_move = nil)
15
+ new(board, previous_move)
16
+ end
17
+ end
18
+
19
+ def current_team
20
+ @board.current_team
21
+ end
22
+
23
+ def complete?
24
+ @board.complete? || next_game_trees.count.zero?
25
+ end
26
+
27
+ def rating(team)
28
+ @board.rating(team)
29
+ end
30
+
31
+ # Produces children based on the number of unique tile collection orientations.
32
+ # For more info, check out Model::TileCollection.
33
+ def next_game_trees
34
+ return @next_game_trees unless @next_game_trees.nil?
35
+
36
+ moves = @board.available_moves
37
+
38
+ @next_game_trees = moves.each_with_object([]) { |move, game_trees| add_game_trees(game_trees, move) }
39
+
40
+ @next_game_trees
41
+ end
42
+
43
+ private
44
+
45
+ def add_game_trees(game_trees, move)
46
+ tile = move.tile
47
+ board = @board.clone
48
+
49
+ board.set_piece(tile.row, tile.col, move.piece)
50
+
51
+ tile_collection = board.tile_collection
52
+
53
+ unless equivalent?(tile_collection.id)
54
+ add_equivalents(tile_collection)
55
+
56
+ board.cycle_teams
57
+
58
+ game_trees << self.class.generate_game_tree(board, move)
59
+ end
60
+
61
+ game_trees
62
+ end
63
+
64
+ def equivalent?(tile_collection_id)
65
+ @equivalent[tile_collection_id]
66
+ end
67
+
68
+ def add_equivalents(tile_collection)
69
+ tile_collection.equivalents.each { |tc| @equivalent[tc.id] = true }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,20 @@
1
+ module Model
2
+ class Move
3
+ attr_reader :tile, :piece
4
+
5
+ def initialize(tile, piece)
6
+ @tile = tile
7
+ @piece = piece
8
+ end
9
+
10
+ class << self
11
+ def generate_moves(piece, board)
12
+ board.available_tiles.map { |tile| generate_move(tile, piece) }
13
+ end
14
+
15
+ def generate_move(tile, piece)
16
+ new(tile, piece)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,86 @@
1
+ # This class uses the MinMax algorithm with alpha/beta pruning
2
+ # This algorithim reflects the process of a player selecting moves
3
+ # That maximize their chances of winning, while another player
4
+ # Selects moves to minimize the other player's chances of winning.
5
+ # MinMax traverses a GameTree in a DFS manner and at each level promotes
6
+ # The highest or lowest GameTree leaf rating, relative to adjacent GameTrees,
7
+ # Depending on whether MinMax is executing in a maximizing or minimizing context.
8
+ # Alpha/beta pruning optimizes this algorithm by preventing redundant branch traversals.
9
+ # Once beta is <= to alpha at any level, MinMax will stop traversing adjacent GameTrees
10
+ # And promote the most optimial GameTree at that level.
11
+ # Since GameTree#next_game_trees is implemented optimally by filtering out equivalent GameTrees, it
12
+ # Improves the performance of MinMax.
13
+ # An additional optimization would be to sort GameTrees by their ratings in ascending order.
14
+ # This would allow MinMax to establish the final beta GameTree rating much quicker, and prune more branches
15
+ # Due to the ascending ordering of the GameTrees.
16
+ # MinMax's worst case runtime is O(n!)
17
+ # For more details: https://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-4-alpha-beta-pruning/
18
+
19
+ module Model
20
+ class MoveStrategy
21
+ EvaluatedBoard = Struct.new(:game_tree, :rating)
22
+
23
+ def select_move(game_tree)
24
+ @team = game_tree.current_team
25
+ evaluated_game_tree = min_max(game_tree, 9, true, -Float::INFINITY, Float::INFINITY)
26
+
27
+ evaluated_game_tree.game_tree.previous_move
28
+ end
29
+
30
+ private
31
+
32
+ def min_max(game_tree, cut_off, maximizing, alpha, beta)
33
+ return evaluated_game_tree(game_tree) if cut_off.zero? || game_tree.complete?
34
+
35
+ cut_off -= 1
36
+
37
+ if maximizing
38
+ max(game_tree.next_game_trees, cut_off, alpha, beta)
39
+ else
40
+ min(game_tree.next_game_trees, cut_off, alpha, beta)
41
+ end
42
+ end
43
+
44
+ def max(game_trees, cut_off, alpha, beta)
45
+ curr_eval_game_tree = nil
46
+
47
+ game_trees.each do |game_tree|
48
+ eval_game_tree = min_max(game_tree, cut_off, false, alpha, beta)
49
+
50
+ eval_game_tree.game_tree = game_tree
51
+
52
+ alpha = eval_game_tree.rating if eval_game_tree.rating > alpha
53
+
54
+ curr_eval_game_tree = eval_game_tree if
55
+ curr_eval_game_tree.nil? || eval_game_tree.rating > curr_eval_game_tree.rating
56
+
57
+ break if beta <= alpha
58
+ end
59
+
60
+ curr_eval_game_tree
61
+ end
62
+
63
+ def min(game_trees, cut_off, alpha, beta)
64
+ curr_eval_game_tree = nil
65
+
66
+ game_trees.each do |game_tree|
67
+ eval_game_tree = min_max(game_tree, cut_off, true, alpha, beta)
68
+
69
+ eval_game_tree.game_tree = game_tree
70
+
71
+ beta = eval_game_tree.rating if eval_game_tree.rating < beta
72
+
73
+ curr_eval_game_tree = eval_game_tree if
74
+ curr_eval_game_tree.nil? || eval_game_tree.rating < curr_eval_game_tree.rating
75
+
76
+ break if beta <= alpha
77
+ end
78
+
79
+ curr_eval_game_tree
80
+ end
81
+
82
+ def evaluated_game_tree(game_tree)
83
+ EvaluatedBoard.new(game_tree, game_tree.rating(@team))
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,15 @@
1
+ module Model
2
+ class Piece
3
+ attr_reader :name
4
+ attr_accessor :team
5
+
6
+ def initialize(name, move_klass)
7
+ @name = name
8
+ @move_klass = move_klass
9
+ end
10
+
11
+ def moves(board)
12
+ @move_klass.generate_moves(self, board)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ module Model
2
+ class Team
3
+ attr_reader :name, :move_strategy, :pieces
4
+
5
+ def initialize(args)
6
+ raise ArgumentError, 'empty pieces array' if args[:pieces].empty?
7
+
8
+ @name = args[:name]
9
+ @move_strategy = args[:move_strategy]
10
+ @pieces = args[:pieces]
11
+
12
+ @pieces.each { |p| p.team = self }
13
+ end
14
+
15
+ def selected_piece
16
+ @pieces[0]
17
+ end
18
+
19
+ def computer?
20
+ !@move_strategy.nil?
21
+ end
22
+
23
+ def available_moves(board)
24
+ @pieces.each_with_object([]) do |piece, moves|
25
+ moves.concat(piece.moves(board))
26
+ end
27
+ end
28
+ end
29
+ end