tic_tac_toes 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
+ 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