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
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
data/.rspec
ADDED
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
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
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,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,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,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
|