tic_tac_toes 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 18a1a5f19c58b04046be154ce6371a79b5c613f2
4
+ data.tar.gz: d856bf4e9b49e86407f173c5a303c0d920202938
5
+ SHA512:
6
+ metadata.gz: 0424458bf16942f7728584c4b4ec040164ce468f8c7a5fb31a0c671c871de601b2d945d46912cce466ab646afce7503dd0ad1d43350e427aa33a0fd4b0fba158
7
+ data.tar.gz: ce07f69665def752ea0f74c959764bab06fdf683bcbe984d4971693df77ad8ce741b48651d728ce3be3382cc1a0a78f13a1433e12accf5c1d41d4f7ce8a54ee6
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .DS_Store
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ *.bundle
20
+ *.so
21
+ *.o
22
+ *.a
23
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ ruby '2.1.1'
3
+ #ruby-gemset=tic_tac_toe
4
+
5
+ # Specify your gem's dependencies in tic_tac_toe.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 bspatafora
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # TicTacToes
2
+
3
+ The game Tic-tac-toe, featuring an unbeatable AI.
4
+
5
+ ## Usage
6
+
7
+ 1. Install the gem: `gem install tic_tac_toes`
8
+ 2. Launch the game: `tic_tac_toes`
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "bundler/gem_tasks"
2
+ require 'pg'
3
+
4
+ task :create_tables do
5
+ connection = establish_connection
6
+ connection.exec("CREATE TABLE games (
7
+ id serial primary key,
8
+ board_size integer,
9
+ winner varchar)")
10
+ connection.exec("CREATE TABLE moves (
11
+ game integer REFERENCES games (id),
12
+ number integer,
13
+ token varchar,
14
+ space integer)")
15
+ end
16
+
17
+ task :destroy_tables do
18
+ connection = establish_connection
19
+ connection.exec("DROP TABLE moves")
20
+ connection.exec("DROP TABLE games")
21
+ end
22
+
23
+ def establish_connection
24
+ PG.connect(dbname: "tic_tac_toes")
25
+ end
data/bin/tic_tac_toes ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'command_line/io'
6
+ require 'command_line/menu'
7
+ require 'database/pg_wrapper'
8
+ require 'tic_tac_toes/io_interface'
9
+ require 'tic_tac_toes/rules'
10
+ require 'tic_tac_toes/history'
11
+
12
+ require 'command_line/runner'
13
+
14
+ database = "tic_tac_toes"
15
+
16
+ io = CommandLine::IO
17
+ io_interface = TicTacToes::IOInterface.new(io)
18
+ menu = CommandLine::Menu.new(io_interface)
19
+ rules = TicTacToes::Rules
20
+ database_interface = Database::PGWrapper.new(database)
21
+ history = TicTacToes::History.new(database_interface)
22
+
23
+ CommandLine::Runner.new(io_interface, menu, rules, history).run
@@ -0,0 +1,27 @@
1
+ module CommandLine
2
+ module IO
3
+ def self.solicit_input
4
+ gets.chomp
5
+ end
6
+
7
+ def self.display(message)
8
+ puts message
9
+ end
10
+
11
+ def self.display_red(message)
12
+ puts red(message)
13
+ end
14
+
15
+ def self.red(message)
16
+ colorize(message, 31)
17
+ end
18
+
19
+ def self.blue(message)
20
+ colorize(message, 34)
21
+ end
22
+
23
+ def self.colorize(message, color_code)
24
+ "\e[#{color_code}m#{message}\e[0m"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ require 'tic_tac_toes/board'
2
+ require 'tic_tac_toes/player_factory'
3
+ require 'tic_tac_toes/rules'
4
+
5
+ module CommandLine
6
+ class Menu
7
+ def initialize(io_interface)
8
+ @io_interface = io_interface
9
+ @player_factory = TicTacToes::PlayerFactory.new(io_interface)
10
+ end
11
+
12
+ def get_board
13
+ TicTacToes::Board.new(row_size: get_row_size)
14
+ end
15
+
16
+ def get_players
17
+ taken_tokens = []
18
+ human_token = get_token(:human, taken_tokens)
19
+ taken_tokens << human_token
20
+ computer_token = get_token(:computer, taken_tokens)
21
+ difficulty = get_difficulty
22
+
23
+ human_player = @player_factory.generate_human_player(human_token)
24
+ computer_player = @player_factory.generate_computer_player(computer_token, difficulty)
25
+ [human_player, computer_player]
26
+ end
27
+
28
+ private
29
+
30
+ def get_row_size
31
+ loop do
32
+ row_size = @io_interface.get_row_size
33
+ break row_size if TicTacToes::Rules.row_size_valid?(row_size)
34
+ @io_interface.invalid_row_size_error
35
+ end
36
+ end
37
+
38
+ def get_token(player, taken_tokens)
39
+ loop do
40
+ token = @io_interface.get_token(player)
41
+ break token if TicTacToes::Rules.token_valid?(token, taken_tokens)
42
+ @io_interface.invalid_token_error
43
+ end
44
+ end
45
+
46
+ def get_difficulty
47
+ loop do
48
+ difficulty = @io_interface.get_difficulty
49
+ break difficulty if TicTacToes::Rules.difficulty_valid?(difficulty)
50
+ @io_interface.invalid_difficulty_error
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ module CommandLine
2
+ class Runner
3
+ def initialize(io_interface, menu, rules, history)
4
+ @io_interface = io_interface
5
+ @menu = menu
6
+ @rules = rules
7
+ @history = history
8
+ end
9
+
10
+ def run
11
+ board, players = @menu.get_board, @menu.get_players
12
+ @history.record_board_size(board.size)
13
+
14
+ take_turn(board, players) until @rules.game_over?(board, players)
15
+ end_game(board, players)
16
+ end
17
+
18
+ def take_turn(board, players)
19
+ @io_interface.draw_board(board)
20
+ @io_interface.thinking_notification if players.first.needs_to_think
21
+
22
+ move = players.first.make_move(board, players)
23
+ @history.record_move(move)
24
+ players.rotate!
25
+ end
26
+
27
+ def end_game(board, players)
28
+ @io_interface.draw_board(board)
29
+
30
+ winner = @rules.determine_winner(board, players)
31
+
32
+ @history.record_winner(winner)
33
+ @history.persist
34
+ @io_interface.game_over_notification(winner)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,72 @@
1
+ require 'pg'
2
+ require 'tic_tac_toes/history'
3
+
4
+ module Database
5
+ class PGWrapper
6
+ def initialize(database)
7
+ @database = database
8
+ end
9
+
10
+ def record_game_history(history)
11
+ connection = establish_connection
12
+ record_board_size_and_winner(history, connection)
13
+ record_moves(history, connection)
14
+ end
15
+
16
+ def read_game_histories
17
+ connection = establish_connection
18
+ games_result = connection.exec("SELECT * FROM games")
19
+ games = []
20
+
21
+ games_result.each_row do |row|
22
+ game_id = row[0]
23
+ board_size = row[1].to_i
24
+ winner = row[2]
25
+ moves_result = connection.exec("SELECT * FROM moves WHERE game = #{game_id}")
26
+
27
+ history = TicTacToes::History.new(self)
28
+
29
+ moves_result.each_row do |row|
30
+ token, space = row[2], row[3].to_i
31
+ history.record_move([token, space])
32
+ end
33
+ history.record_board_size(board_size)
34
+ history.record_winner(winner)
35
+
36
+ games << history
37
+ end
38
+
39
+ games
40
+ end
41
+
42
+ private
43
+
44
+ def establish_connection
45
+ PG.connect(dbname: @database)
46
+ end
47
+
48
+ def record_board_size_and_winner(history, connection)
49
+ connection.exec("INSERT INTO games (board_size, winner) VALUES (
50
+ #{history.board_size},
51
+ '#{history.winner}')")
52
+ end
53
+
54
+ def record_moves(history, connection)
55
+ game_id = read_game_id(connection)
56
+
57
+ history.moves.each_with_index do |move, index|
58
+ move_number = index + 1
59
+ connection.exec("INSERT INTO moves (game, number, token, space) VALUES (
60
+ #{game_id},
61
+ #{move_number},
62
+ '#{move.first}',
63
+ #{move.last})")
64
+ end
65
+ end
66
+
67
+ def read_game_id(connection)
68
+ result = connection.exec("SELECT currval(pg_get_serial_sequence('games','id'))")
69
+ result.getvalue(0,0)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,62 @@
1
+ module TicTacToes
2
+ class Board
3
+ attr_reader :row_size, :size
4
+
5
+ def initialize(row_size: 3)
6
+ @row_size = row_size
7
+ @size = @row_size**2
8
+ @spaces = Array.new(@size)
9
+ end
10
+
11
+ def place(player, space)
12
+ @spaces[space] = player if valid?(space)
13
+ end
14
+
15
+ def space(space)
16
+ @spaces[space]
17
+ end
18
+
19
+ def open_spaces
20
+ open_spaces = []
21
+
22
+ @spaces.each_with_index do |space, index|
23
+ open_spaces << index if space.nil?
24
+ end
25
+
26
+ open_spaces
27
+ end
28
+
29
+ def rows
30
+ @spaces.each_slice(@row_size).to_a
31
+ end
32
+
33
+ def columns
34
+ rows.transpose
35
+ end
36
+
37
+ def diagonals
38
+ back_diagonal, front_diagonal = [], []
39
+
40
+ rows.each_with_index do |row, index|
41
+ back_diagonal << row[index]
42
+ front_diagonal << row[@row_size - (index + 1)]
43
+ end
44
+
45
+ [back_diagonal, front_diagonal]
46
+ end
47
+
48
+ def full?
49
+ @spaces.all? { |space| !space.nil? }
50
+ end
51
+
52
+ private
53
+
54
+ def valid?(space)
55
+ space_empty = @spaces[space].nil?
56
+ board_range = 0..(@size - 1)
57
+ on_the_board = board_range.include? space
58
+
59
+ space_empty && on_the_board
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ require 'tic_tac_toes/board'
2
+
3
+ module TicTacToes
4
+ module EasyAI
5
+ def self.make_move(board, _players)
6
+ board.open_spaces.sample
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ require 'tic_tac_toes/rules'
2
+
3
+ module TicTacToes
4
+ module HardAI
5
+ def self.make_move(board, players)
6
+ open_spaces = Hash[board.open_spaces.map { |space| [space, nil] }]
7
+
8
+ open_spaces.each do |space, score|
9
+ score = minimax(generate_board(players.first, space, board), :min, players)
10
+ open_spaces[space] = score
11
+ end
12
+
13
+ best_score = open_spaces.values.max
14
+ open_spaces.each { |space, score| return space if score == best_score }
15
+ end
16
+
17
+ def self.minimax(board, current_player, players)
18
+ return score(board, players) if Rules.game_over?(board, players)
19
+
20
+ if current_player == :max
21
+ best_score = -1
22
+ board.open_spaces.each do |space|
23
+ score = minimax(generate_board(players.first, space, board), :min, players)
24
+ best_score = [best_score, score].max
25
+ end
26
+ best_score
27
+
28
+ elsif current_player == :min
29
+ best_score = 1
30
+ board.open_spaces.each do |space|
31
+ score = minimax(generate_board(players.last, space, board), :max, players)
32
+ best_score = [best_score, score].min
33
+ end
34
+ best_score
35
+ end
36
+ end
37
+
38
+ def self.generate_board(player, space, board)
39
+ new_board = Marshal.load(Marshal.dump(board))
40
+ new_board.place(player, space)
41
+ new_board
42
+ end
43
+
44
+ def self.score(board, players)
45
+ own_token, opponent_token = players.first.token, players.last.token
46
+
47
+ case Rules.determine_winner(board, players)
48
+ when own_token
49
+ 1
50
+ when opponent_token
51
+ -1
52
+ else
53
+ 0
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ module TicTacToes
2
+ class History
3
+ attr_reader :board_size, :moves, :winner
4
+
5
+ def initialize(database_interface)
6
+ @database_interface = database_interface
7
+ end
8
+
9
+ def record_board_size(size)
10
+ @board_size = size
11
+ end
12
+
13
+ def record_move(move)
14
+ @moves ||= []
15
+ @moves << move
16
+ end
17
+
18
+ def record_winner(winner)
19
+ winner = "Draw" if winner.nil?
20
+ @winner = winner
21
+ end
22
+
23
+ def persist
24
+ @database_interface.record_game_history(self)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,96 @@
1
+ require 'tic_tac_toes/strings'
2
+
3
+ module TicTacToes
4
+ class IOInterface
5
+ def initialize(io)
6
+ @io = io
7
+ end
8
+
9
+ def make_move(_board, _players)
10
+ move_solicitation
11
+
12
+ Integer(@io.solicit_input)
13
+ rescue ArgumentError
14
+ not_an_integer_error
15
+ make_move(_board, _players)
16
+ end
17
+
18
+ def get_row_size
19
+ row_size_solicitation
20
+
21
+ Integer(@io.solicit_input)
22
+ rescue ArgumentError
23
+ not_an_integer_error
24
+ get_row_size
25
+ end
26
+
27
+ def get_token(player)
28
+ token_solicitation(player)
29
+ @io.solicit_input
30
+ end
31
+
32
+ def get_difficulty
33
+ difficulty_solicitation
34
+ @io.solicit_input.downcase.to_sym
35
+ end
36
+
37
+ def draw_board(board)
38
+ @io.display(Strings.board(board))
39
+ end
40
+
41
+ def invalid_row_size_error
42
+ @io.display_red(Strings::INVALID_ROW_SIZE)
43
+ end
44
+
45
+ def invalid_token_error
46
+ @io.display_red(Strings::INVALID_TOKEN)
47
+ end
48
+
49
+ def invalid_difficulty_error
50
+ @io.display_red(Strings::INVALID_DIFFICULTY)
51
+ end
52
+
53
+ def invalid_move_error
54
+ @io.display_red(Strings::INVALID_MOVE)
55
+ end
56
+
57
+ def thinking_notification
58
+ @io.display_red(Strings::THINKING)
59
+ end
60
+
61
+ def game_over_notification(winner)
62
+ winner = "Nobody" if winner.nil?
63
+ @io.display(Strings.game_over_notification(winner))
64
+ end
65
+
66
+ def red(message)
67
+ @io.red(message)
68
+ end
69
+
70
+ def blue(message)
71
+ @io.blue(message)
72
+ end
73
+
74
+ private
75
+
76
+ def move_solicitation
77
+ @io.display(Strings::MOVE_SOLICITATION)
78
+ end
79
+
80
+ def row_size_solicitation
81
+ @io.display(Strings::ROW_SIZE_SOLICITATION)
82
+ end
83
+
84
+ def token_solicitation(player)
85
+ @io.display(Strings.token_solicitation(player))
86
+ end
87
+
88
+ def difficulty_solicitation
89
+ @io.display(Strings::DIFFICULTY_SOLICITATION)
90
+ end
91
+
92
+ def not_an_integer_error
93
+ @io.display_red(Strings::NOT_AN_INTEGER)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ require 'tic_tac_toes/easy_ai'
2
+ require 'tic_tac_toes/hard_ai'
3
+
4
+ module TicTacToes
5
+ module MediumAI
6
+ def self.make_move(board, players)
7
+ ai = [EasyAI, HardAI, HardAI].sample
8
+ ai.make_move(board, players)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module TicTacToes
2
+ class Player
3
+ attr_reader :decider, :token, :needs_to_think
4
+
5
+ def initialize(decider, token, needs_to_think, io_interface)
6
+ @decider = decider
7
+ @token = token
8
+ @needs_to_think = needs_to_think
9
+ @io_interface = io_interface
10
+ end
11
+
12
+ def make_move(board, players)
13
+ loop do
14
+ space = @decider.make_move(board, players)
15
+ break [@token, space] if board.place(self, space)
16
+ @io_interface.invalid_move_error
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ require 'tic_tac_toes/easy_ai'
2
+ require 'tic_tac_toes/hard_ai'
3
+ require 'tic_tac_toes/medium_ai'
4
+ require 'tic_tac_toes/player'
5
+
6
+ module TicTacToes
7
+ class PlayerFactory
8
+ AI_DIFFICULTIES = { easy: EasyAI, medium: MediumAI, hard: HardAI }
9
+
10
+ def initialize(io_interface)
11
+ @io_interface = io_interface
12
+ end
13
+
14
+ def generate_human_player(token)
15
+ needs_to_think = false
16
+ Player.new(@io_interface, token, needs_to_think, @io_interface)
17
+ end
18
+
19
+ def generate_computer_player(token, difficulty)
20
+ needs_to_think = true
21
+ Player.new(AI_DIFFICULTIES[difficulty], token, needs_to_think, @io_interface)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,64 @@
1
+ require 'tic_tac_toes/player_factory'
2
+
3
+ module TicTacToes
4
+ module Rules
5
+ ROW_SIZE_RANGE = (2..10)
6
+
7
+ def self.row_size_valid?(row_size)
8
+ row_size.between?(ROW_SIZE_RANGE.min, ROW_SIZE_RANGE.max)
9
+ end
10
+
11
+ def self.token_valid?(token, taken_tokens)
12
+ correct_length = token.length == 1
13
+ untaken = !taken_tokens.include?(token)
14
+
15
+ correct_length && untaken
16
+ end
17
+
18
+ def self.difficulty_valid?(difficulty)
19
+ PlayerFactory::AI_DIFFICULTIES.include? difficulty
20
+ end
21
+
22
+ def self.game_over?(board, players)
23
+ winner = !determine_winner(board, players).nil?
24
+ tie = board.full?
25
+
26
+ winner || tie
27
+ end
28
+
29
+ def self.determine_winner(board, players)
30
+ winner = nil
31
+
32
+ players.each do |player|
33
+ player_has_won = win?(board, player)
34
+ winner = player.token if player_has_won
35
+ end
36
+
37
+ winner
38
+ end
39
+
40
+ def self.win?(board, player)
41
+ diagonal_win?(board, player) ||
42
+ horizontal_win?(board, player) ||
43
+ vertical_win?(board, player)
44
+ end
45
+
46
+ private
47
+
48
+ def self.diagonal_win?(board, player)
49
+ set_win?(board.diagonals, player)
50
+ end
51
+
52
+ def self.horizontal_win?(board, player)
53
+ set_win?(board.rows, player)
54
+ end
55
+
56
+ def self.vertical_win?(board, player)
57
+ set_win?(board.columns, player)
58
+ end
59
+
60
+ def self.set_win?(sets, player)
61
+ sets.any? { |set| set.all? { |space| space.token == player.token unless space.nil? } }
62
+ end
63
+ end
64
+ end