ttt-cli 0.1.4
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +45 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/ttt-cli +41 -0
- data/lib/ttt-cli.rb +16 -0
- data/lib/ttt-cli/ai.rb +76 -0
- data/lib/ttt-cli/board.rb +70 -0
- data/lib/ttt-cli/display.rb +101 -0
- data/lib/ttt-cli/emoji.rb +7 -0
- data/lib/ttt-cli/engine.rb +76 -0
- data/lib/ttt-cli/game.rb +137 -0
- data/lib/ttt-cli/judge.rb +36 -0
- data/lib/ttt-cli/player.rb +17 -0
- data/lib/ttt-cli/players/computer.rb +29 -0
- data/lib/ttt-cli/players/human.rb +25 -0
- data/lib/ttt-cli/strategies/hard_ai.rb +12 -0
- data/lib/ttt-cli/strategies/medium_ai.rb +12 -0
- data/lib/ttt-cli/strategies/random_ai.rb +7 -0
- data/lib/ttt-cli/version.rb +5 -0
- data/spec/ai_spec.rb +42 -0
- data/spec/board_spec.rb +38 -0
- data/spec/game_spec.rb +176 -0
- data/spec/judge_spec.rb +18 -0
- data/spec/spec_helper.rb +100 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fb849f2253377ff35938474e78c77feedaff258619d0236495c01174a87876e9
|
4
|
+
data.tar.gz: 72987ee85da5b4888d03b09136720baa956fa0c53a5a7639fdeb6d65f0380cf7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9b20b7ec55fefb1b92bcaeb1233dca660b7c9a36f2a495ccffdf3bcafdd9012295bcc582c5e98f7987a1c004c40334012ce71b1381b2b033ebc320515a123322
|
7
|
+
data.tar.gz: 2c472ce566f303a26ace4fc4bc09118353b3d255dbcc92f6663286d2639acc3878585e60e0c7dd7e9550bf9dcf48b944f1a9132ef14fedb8af40f73e713ec48e
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 dionixs
|
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,45 @@
|
|
1
|
+
# Tic-Tac-Toe
|
2
|
+
|
3
|
+
Консольная реализация игры: "**Крестики-нолики**".
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
Особенности:
|
8
|
+
|
9
|
+
- 3 игровых режима: **Single-player**, **Hot-seat**, **Observer**.
|
10
|
+
- 3 уровня сложности.
|
11
|
+
- **Непобедимый AI** на уровне сложности **Hard**.
|
12
|
+
|
13
|
+
- **Emoji** в качестве игровых символов.
|
14
|
+
- Счётчик побед и поражений.
|
15
|
+
|
16
|
+
## Установка
|
17
|
+
|
18
|
+
Установка пакета `tty-cli` из [Rubygems](https://rubygems.org/gems/ttt-cli):
|
19
|
+
|
20
|
+
```
|
21
|
+
$> gem install ttt-cli
|
22
|
+
```
|
23
|
+
|
24
|
+
## Запуск
|
25
|
+
|
26
|
+
```bash
|
27
|
+
$> ttt-cli
|
28
|
+
```
|
29
|
+
|
30
|
+
## Тесты
|
31
|
+
|
32
|
+
```bash
|
33
|
+
$> bundle exec rake spec
|
34
|
+
```
|
35
|
+
|
36
|
+
## Ресурсы
|
37
|
+
|
38
|
+
* [Крестики-нолики](https://ru.wikipedia.org/wiki/%D0%9A%D1%80%D0%B5%D1%81%D1%82%D0%B8%D0%BA%D0%B8-%D0%BD%D0%BE%D0%BB%D0%B8%D0%BA%D0%B8)
|
39
|
+
* [Let's build the classic game using object-orientation](https://www.vikingcodeschool.com/professional-development-with-ruby/tic-tac-toe)
|
40
|
+
* [Tic Tac Toe: Understanding the Minimax Algorithm](https://www.neverstopbuilding.com/blog/minimax)
|
41
|
+
* [Как написать бота, которого будет нельзя обыграть в «крестики-нолики»](https://tproger.ru/translations/tic-tac-toe-minimax/)
|
42
|
+
|
43
|
+
## License
|
44
|
+
|
45
|
+
Code released under [MIT](https://github.com/dionixs/ttt-cli/blob/master/LICENSE) license.
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'ttt/cli'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bin/ttt-cli
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# encoding: utf-8
|
5
|
+
|
6
|
+
require 'ttt-cli'
|
7
|
+
|
8
|
+
# Cтарт новой игры:
|
9
|
+
# - Выводим приветственный текст
|
10
|
+
# - Устанавливаем режим игры
|
11
|
+
# - Устанавливаем уровень сложности игры
|
12
|
+
# - Устанавливаем символы игрокам
|
13
|
+
# - Определяем кто ходит первым
|
14
|
+
# - Выводим игровое поле на экран
|
15
|
+
game = Game.start
|
16
|
+
|
17
|
+
# основной цикл:
|
18
|
+
# -- пока поле не заполнилось или один из игроков не победил
|
19
|
+
loop do
|
20
|
+
# ход текущего игрока
|
21
|
+
game.current_player.make_move(game.board)
|
22
|
+
# вывод игрового поля на экран
|
23
|
+
CommandLine::Display.print_board(game.board)
|
24
|
+
|
25
|
+
# проверка ситуации на поле
|
26
|
+
if game.over?
|
27
|
+
# вывод сообщения о победе/ничье.
|
28
|
+
game.over_message
|
29
|
+
# спрашиваем игрока, хочет ли он сыграть еще
|
30
|
+
if game.start_new_game?
|
31
|
+
# создаем новую игру
|
32
|
+
game = Game.start
|
33
|
+
next
|
34
|
+
else
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# переключение на следующего игрока
|
40
|
+
game.switch_player
|
41
|
+
end
|
data/lib/ttt-cli.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'ttt-cli/version'
|
4
|
+
require_relative 'ttt-cli/emoji'
|
5
|
+
require_relative 'ttt-cli/display'
|
6
|
+
require_relative 'ttt-cli/board'
|
7
|
+
require_relative 'ttt-cli/ai'
|
8
|
+
require_relative 'ttt-cli/strategies/random_ai'
|
9
|
+
require_relative 'ttt-cli/strategies/medium_ai'
|
10
|
+
require_relative 'ttt-cli/strategies/hard_ai'
|
11
|
+
require_relative 'ttt-cli/player'
|
12
|
+
require_relative 'ttt-cli/players/computer'
|
13
|
+
require_relative 'ttt-cli/players/human'
|
14
|
+
require_relative 'ttt-cli/judge'
|
15
|
+
require_relative 'ttt-cli/engine'
|
16
|
+
require_relative 'ttt-cli/game'
|
data/lib/ttt-cli/ai.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AI
|
4
|
+
attr_accessor :best_move
|
5
|
+
|
6
|
+
def self.create(game, type, computer)
|
7
|
+
type.new(game, computer)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(game, computer)
|
11
|
+
@game = game
|
12
|
+
@board = game.board
|
13
|
+
@computer = computer
|
14
|
+
@enemy = @game.first_player
|
15
|
+
@best_move = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Метод для генерации хода AI
|
19
|
+
def move_generate; end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def is_first_move?
|
24
|
+
@board.empty_cells.size == 9 ||
|
25
|
+
@board.empty_cells.size == 8
|
26
|
+
end
|
27
|
+
|
28
|
+
# основная минимакс-функция
|
29
|
+
# возвращает значение, если найдено конечное состояние(+10, 0, -10)
|
30
|
+
# проходит по всем пустым клеткам на поле
|
31
|
+
# вызывает минимакс функцию для каждой из них (рекурсия)
|
32
|
+
# оценивает полученные значения
|
33
|
+
# и возвращает наилучшее из них
|
34
|
+
def minimax(board, player, depth = 0, moves = {})
|
35
|
+
# копия доски
|
36
|
+
new_board = board.dup
|
37
|
+
# индексы свободных клеток
|
38
|
+
empty_indices = new_board.empty_cells
|
39
|
+
|
40
|
+
# проверка на терминальное состояние
|
41
|
+
return 10 - depth if @game.won?(@computer)
|
42
|
+
return -10 + depth if @game.won?(@enemy)
|
43
|
+
return 0 if @game.draw?
|
44
|
+
|
45
|
+
empty_indices.each do |index|
|
46
|
+
new_board.fill_cell(index, player.token)
|
47
|
+
next_player = opponent(player)
|
48
|
+
moves[index] = minimax(new_board, next_player, depth + 1)
|
49
|
+
new_board.reset_cell(index)
|
50
|
+
end
|
51
|
+
|
52
|
+
# возвращаем наилучшее значение
|
53
|
+
minimax_score(moves, player)
|
54
|
+
end
|
55
|
+
|
56
|
+
# метод для оценки полученных значений
|
57
|
+
# возвращает наилучшее значение
|
58
|
+
def minimax_score(moves, player)
|
59
|
+
if player.token == @computer.token
|
60
|
+
@best_move, best_score = moves.max_by { |_, v| v }
|
61
|
+
else
|
62
|
+
@best_move, best_score = moves.min_by { |_, v| v }
|
63
|
+
end
|
64
|
+
|
65
|
+
best_score
|
66
|
+
end
|
67
|
+
|
68
|
+
# метод для выбора оппонента для текущего игрока
|
69
|
+
def opponent(player)
|
70
|
+
if player.token == @computer.token
|
71
|
+
@enemy
|
72
|
+
else
|
73
|
+
@computer
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rainbow'
|
4
|
+
|
5
|
+
# Класс Board - игровое поле.
|
6
|
+
#
|
7
|
+
# По умолчанию имеет размер 3х3.
|
8
|
+
class Board
|
9
|
+
attr_accessor :cells
|
10
|
+
attr_reader :game
|
11
|
+
|
12
|
+
include Emoji
|
13
|
+
|
14
|
+
def initialize(game)
|
15
|
+
@cells = Array.new(9, DASH)
|
16
|
+
@game = game
|
17
|
+
end
|
18
|
+
|
19
|
+
# метод для заполнения клетки
|
20
|
+
def fill_cell(index, token)
|
21
|
+
@cells[index] = token
|
22
|
+
end
|
23
|
+
|
24
|
+
# метод для проверки клетки на занятость
|
25
|
+
def cell_taken?(index)
|
26
|
+
@cells[index] == X || @cells[index] == O
|
27
|
+
end
|
28
|
+
|
29
|
+
# метод для проверки всех клеток на занятость
|
30
|
+
def full?
|
31
|
+
@cells.all? do |char|
|
32
|
+
if char == X || char == O
|
33
|
+
true
|
34
|
+
else
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# метод возвращает массив индексов пустых клеток доски
|
41
|
+
def empty_cells
|
42
|
+
@cells.filter_map.with_index do |cell, index|
|
43
|
+
index if cell != X && cell != O
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# метод возвращает массив четных индексов клеток доски
|
48
|
+
def even_cells
|
49
|
+
empty_cells.filter(&:even?)
|
50
|
+
end
|
51
|
+
|
52
|
+
# метод для очистки клетки
|
53
|
+
def reset_cell(index)
|
54
|
+
@cells[index] = DASH
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
Rainbow("
|
59
|
+
| |
|
60
|
+
#{@cells[0]} | #{@cells[1]} | #{@cells[2]}
|
61
|
+
______|______|______
|
62
|
+
| |
|
63
|
+
#{@cells[3]} | #{@cells[4]} | #{@cells[5]}
|
64
|
+
______|______|______
|
65
|
+
| |
|
66
|
+
#{@cells[6]} | #{@cells[7]} | #{@cells[8]}
|
67
|
+
| |
|
68
|
+
").lawngreen
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rainbow'
|
4
|
+
require 'tty-prompt'
|
5
|
+
require 'tty-table'
|
6
|
+
|
7
|
+
module CommandLine
|
8
|
+
class Display
|
9
|
+
def self.prompt
|
10
|
+
TTY::Prompt.new(interrupt: :exit)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.welcome_banner
|
14
|
+
clear
|
15
|
+
puts Rainbow("Welcome to Tic Tac Toe!\n").lawngreen
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.clear
|
19
|
+
system('clear') || system('cls')
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.difficulty
|
23
|
+
levels = %w[easy medium hard]
|
24
|
+
prompt.select(
|
25
|
+
'Select a difficulty level:',
|
26
|
+
levels, symbols: { marker: '>' }
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.game_mode
|
31
|
+
modes = %w[singleplayer hotseat observer]
|
32
|
+
prompt.select(
|
33
|
+
'Choose game mode:',
|
34
|
+
modes, symbols: { marker: '>' }
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.user_token(tokens = %w[X O])
|
39
|
+
prompt.select(
|
40
|
+
'Do you want to be X or O?',
|
41
|
+
tokens, symbols: { marker: '>' }
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.choose_cell(choice = nil)
|
46
|
+
moves = Array(1..9).map(&:to_s)
|
47
|
+
while !moves.include?(choice) && choice != 'exit'
|
48
|
+
print Rainbow('What is your move? (1-9): ').lawngreen
|
49
|
+
choice = STDIN.gets.strip
|
50
|
+
end
|
51
|
+
exit if choice == 'exit'
|
52
|
+
choice.to_i - 1
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.winner(game)
|
56
|
+
if game.game_mode == :singleplayer
|
57
|
+
puts Rainbow("\tYou Win!\n").deepskyblue
|
58
|
+
else
|
59
|
+
puts Rainbow("#{game.current_player.name} has won the game!\n").deepskyblue
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.draw
|
64
|
+
puts Rainbow("\tDraw!\n").yellow
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.play_again
|
68
|
+
prompt.yes?('Would you to play again?')
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.loser(game)
|
72
|
+
if game.game_mode == :singleplayer
|
73
|
+
puts Rainbow("\tYou Lose!\n").red
|
74
|
+
else
|
75
|
+
winner(game)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.logo
|
80
|
+
"
|
81
|
+
┌───────────────┐
|
82
|
+
│ Tic-Tac-Toe │
|
83
|
+
└───────────────┘"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.scoreboard(game)
|
87
|
+
table = TTY::Table.new [game.first_player.name, ' Tie ', game.second_player.name],
|
88
|
+
[[game.wins, game.draws, game.losses]]
|
89
|
+
scoreboard = table.render :unicode, alignment: [:center]
|
90
|
+
puts Rainbow(scoreboard).lawngreen
|
91
|
+
puts
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.print_board(board)
|
95
|
+
clear
|
96
|
+
puts Rainbow(logo).lawngreen
|
97
|
+
puts board
|
98
|
+
scoreboard(board.game)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Engine
|
4
|
+
attr_accessor :difficulty
|
5
|
+
|
6
|
+
@@game_mode = :singleplayer
|
7
|
+
|
8
|
+
@@draws = 0
|
9
|
+
@@wins = 0
|
10
|
+
@@losses = 0
|
11
|
+
|
12
|
+
include Emoji
|
13
|
+
|
14
|
+
DIFFICULTY_LEVELS = {
|
15
|
+
easy: RandomAI,
|
16
|
+
medium: MediumAI,
|
17
|
+
hard: HardAI
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def self.set_game_mode(mode) # todo
|
21
|
+
if Engine.is_singleplayer?(mode)
|
22
|
+
Engine.reset_counters if @@game_mode == :hotseat || @@game_mode == :observer
|
23
|
+
@@game_mode = :singleplayer
|
24
|
+
elsif mode == :hotseat
|
25
|
+
Engine.reset_counters if @@game_mode == :singleplayer || @@game_mode == :observer
|
26
|
+
@@game_mode = :hotseat
|
27
|
+
else
|
28
|
+
Engine.reset_counters if @@game_mode == :singleplayer || @@game_mode == :hotseat
|
29
|
+
@@game_mode = :observer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.is_singleplayer?(mode)
|
34
|
+
mode == :singleplayer
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.set_difficulty
|
38
|
+
if @@game_mode == :singleplayer
|
39
|
+
Engine.difficulty_level
|
40
|
+
else
|
41
|
+
HardAI
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.game_mode
|
46
|
+
mode = CommandLine::Display.game_mode
|
47
|
+
mode.to_sym
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.difficulty_level
|
51
|
+
level = CommandLine::Display.difficulty
|
52
|
+
DIFFICULTY_LEVELS[level.to_sym]
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.reset_counters
|
56
|
+
@@draws = 0
|
57
|
+
@@losses = 0
|
58
|
+
@@wins = 0
|
59
|
+
end
|
60
|
+
|
61
|
+
def game_mode
|
62
|
+
@@game_mode
|
63
|
+
end
|
64
|
+
|
65
|
+
def wins
|
66
|
+
@@wins
|
67
|
+
end
|
68
|
+
|
69
|
+
def draws
|
70
|
+
@@draws
|
71
|
+
end
|
72
|
+
|
73
|
+
def losses
|
74
|
+
@@losses
|
75
|
+
end
|
76
|
+
end
|
data/lib/ttt-cli/game.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Класс Game: Управляет игровым процессом
|
4
|
+
class Game < Engine
|
5
|
+
attr_accessor :first_player, :second_player,
|
6
|
+
:current_player, :judge
|
7
|
+
attr_reader :board
|
8
|
+
|
9
|
+
def self.start
|
10
|
+
CommandLine::Display.welcome_banner
|
11
|
+
Game.set_game_mode(Game.game_mode)
|
12
|
+
new_game = new(Game.set_difficulty)
|
13
|
+
Game.setup_game(new_game)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.setup_game(game)
|
17
|
+
game.update_players!
|
18
|
+
game.set_players_tokens(game.get_user_token)
|
19
|
+
game.who_goes_first
|
20
|
+
CommandLine::Display.print_board(game.board)
|
21
|
+
game
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(difficulty = MediumAI)
|
25
|
+
@board = Board.new(self)
|
26
|
+
@difficulty = difficulty
|
27
|
+
@first_player = Players::Human.new(token: X)
|
28
|
+
@second_player = Players::Computer.new(token: O, game: self)
|
29
|
+
@current_player = @first_player
|
30
|
+
@judge = Judge.new(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_new_game?
|
34
|
+
CommandLine::Display.play_again
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_players!
|
38
|
+
if @@game_mode == :singleplayer
|
39
|
+
singleplayer_mode
|
40
|
+
elsif @@game_mode == :hotseat
|
41
|
+
hotseat_mode
|
42
|
+
else
|
43
|
+
observer_mode
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def singleplayer_mode
|
48
|
+
@second_player = Players::Computer.new(token: O, game: self)
|
49
|
+
end
|
50
|
+
|
51
|
+
def hotseat_mode
|
52
|
+
@first_player = Players::Human.new(token: X, name: 'Player 1')
|
53
|
+
@second_player = Players::Human.new(token: O, name: 'Player 2')
|
54
|
+
@current_player = @first_player
|
55
|
+
end
|
56
|
+
|
57
|
+
def observer_mode
|
58
|
+
@first_player = Players::Computer.new(token: X, name: 'Connor', game: self)
|
59
|
+
@second_player = Players::Computer.new(token: O, name: 'William', game: self)
|
60
|
+
@current_player = @first_player
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_user_token
|
64
|
+
if @@game_mode == :singleplayer
|
65
|
+
CommandLine::Display.user_token
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Метод который устанавливает символы игрокам
|
70
|
+
# По умолчанию игрок - X, компьютер - O
|
71
|
+
def set_players_tokens(token)
|
72
|
+
if @@game_mode == :singleplayer
|
73
|
+
if token != 'X'
|
74
|
+
@first_player.token = O
|
75
|
+
@second_player.token = X
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Метод для выбора игрока, который будет ходить первым.
|
81
|
+
# По умолчанию первым ходит игрок.
|
82
|
+
def who_goes_first
|
83
|
+
case @first_player.token
|
84
|
+
when O
|
85
|
+
@current_player = @second_player
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Метод для переключения на следующего игрока
|
90
|
+
def switch_player
|
91
|
+
case @current_player
|
92
|
+
when @first_player
|
93
|
+
@current_player = @second_player
|
94
|
+
else
|
95
|
+
@current_player = @first_player
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def over?
|
100
|
+
draw? || won?
|
101
|
+
end
|
102
|
+
|
103
|
+
def draw?
|
104
|
+
@board.full? && !won?
|
105
|
+
end
|
106
|
+
|
107
|
+
def won?(player = @current_player)
|
108
|
+
@judge.is_combo?(player)
|
109
|
+
end
|
110
|
+
|
111
|
+
def over_message
|
112
|
+
increase_counter
|
113
|
+
CommandLine::Display.print_board(@board)
|
114
|
+
print_winner
|
115
|
+
end
|
116
|
+
|
117
|
+
# Метод для объявления победителя
|
118
|
+
def print_winner
|
119
|
+
if draw?
|
120
|
+
CommandLine::Display.draw
|
121
|
+
elsif won?(@first_player)
|
122
|
+
CommandLine::Display.winner(self)
|
123
|
+
elsif won?(@second_player)
|
124
|
+
CommandLine::Display.loser(self)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def increase_counter
|
129
|
+
if draw?
|
130
|
+
@@draws += 1
|
131
|
+
elsif won?(@first_player)
|
132
|
+
@@wins += 1
|
133
|
+
elsif won?(@second_player)
|
134
|
+
@@losses += 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Класс Judge - судья (проверщик).
|
4
|
+
class Judge
|
5
|
+
attr_accessor :board
|
6
|
+
|
7
|
+
WIN_COMBINATIONS = [
|
8
|
+
[0, 1, 2], [3, 4, 5], [6, 7, 8],
|
9
|
+
[0, 3, 6], [1, 4, 7], [2, 5, 8],
|
10
|
+
[0, 4, 8], [2, 4, 6]
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
def initialize(game)
|
14
|
+
@board = game.board.cells
|
15
|
+
end
|
16
|
+
|
17
|
+
# основной метод для проверки на выигрыш
|
18
|
+
# возвращает true если обнаружена выигрышная комбинация
|
19
|
+
def is_combo?(player)
|
20
|
+
!winning_combination(player.token).nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# метод который возвращает первую подходящую комбинацию
|
26
|
+
# или nil если не было совпадения
|
27
|
+
def winning_combination(token)
|
28
|
+
WIN_COMBINATIONS.find do |indices|
|
29
|
+
# возвращаем значения для соответствующих индексов
|
30
|
+
# values_at(*[0,1,2]) => values_at(0,1,2)
|
31
|
+
values = @board.values_at(*indices)
|
32
|
+
# проверяем, равны ли все значения X или O
|
33
|
+
values.all?(token)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Класс Player (abstract class)
|
4
|
+
class Player
|
5
|
+
attr_accessor :board, :token, :name, :enemy
|
6
|
+
|
7
|
+
include Emoji
|
8
|
+
|
9
|
+
def initialize(params)
|
10
|
+
@name = params[:name]
|
11
|
+
@token = params[:token]
|
12
|
+
end
|
13
|
+
|
14
|
+
def make_move(board); end
|
15
|
+
|
16
|
+
def position; end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Players
|
4
|
+
class Computer < Player
|
5
|
+
attr_reader :difficulty
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
super(params)
|
9
|
+
@name = 'Computer' unless params[:name]
|
10
|
+
@game = params[:game]
|
11
|
+
@board = @game.board
|
12
|
+
@enemy = @game.first_player unless params[:enemy]
|
13
|
+
@difficulty = @game.difficulty
|
14
|
+
@ai = AI.create(@game, @difficulty, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def make_move(board)
|
18
|
+
move = position
|
19
|
+
sleep 0.6
|
20
|
+
board.fill_cell(move, @token) unless board.cell_taken?(move)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def position
|
26
|
+
@ai.move_generate
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Players
|
4
|
+
class Human < Player
|
5
|
+
def initialize(params)
|
6
|
+
super(params)
|
7
|
+
@name = 'Human' unless params[:name]
|
8
|
+
end
|
9
|
+
|
10
|
+
def make_move(board)
|
11
|
+
move = position
|
12
|
+
if !board.cell_taken?(move)
|
13
|
+
board.fill_cell(move, @token)
|
14
|
+
else
|
15
|
+
make_move(board)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def position
|
22
|
+
CommandLine::Display.choose_cell
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/spec/ai_spec.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ttt-cli'
|
4
|
+
|
5
|
+
include Emoji
|
6
|
+
|
7
|
+
describe AI do
|
8
|
+
before { @game = Game.new }
|
9
|
+
|
10
|
+
it 'should create RandomAI' do
|
11
|
+
ai = AI.create(@game, RandomAI, @game.second_player)
|
12
|
+
expect(ai.class).to eq RandomAI
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should create MediumAI' do
|
16
|
+
ai = AI.create(@game, MediumAI, @game.second_player)
|
17
|
+
expect(ai.class).to eq MediumAI
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should create HardAI' do
|
21
|
+
ai = AI.create(@game, HardAI, @game.second_player)
|
22
|
+
expect(ai.class).to eq HardAI
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'move RandomAI' do
|
26
|
+
ai = AI.create(@game, RandomAI, @game.second_player)
|
27
|
+
move = ai.move_generate
|
28
|
+
expect(move.between?(0, 8)).to eq true
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'move MediumAI' do
|
32
|
+
ai = AI.create(@game, MediumAI, @game.second_player)
|
33
|
+
move = ai.move_generate
|
34
|
+
expect(move.between?(0, 8)).to eq true
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'move HardAI' do
|
38
|
+
ai = AI.create(@game, HardAI, @game.second_player)
|
39
|
+
move = ai.move_generate
|
40
|
+
expect(move.between?(0, 8)).to eq true
|
41
|
+
end
|
42
|
+
end
|
data/spec/board_spec.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ttt-cli'
|
4
|
+
|
5
|
+
include Emoji
|
6
|
+
|
7
|
+
describe Board do
|
8
|
+
before { @game = Game.new }
|
9
|
+
|
10
|
+
it 'fill cell' do
|
11
|
+
@game.board.fill_cell(0, X)
|
12
|
+
expect(@game.board.cells[0]).to eq X
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'check taken cell' do
|
16
|
+
@game.board.fill_cell(0, X)
|
17
|
+
expect(@game.board.cell_taken?(0)).to eq true
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'occupied cells' do
|
21
|
+
@game.board.cells = [X, O, X, X, X, O, O, X, O]
|
22
|
+
expect(@game.board.full?).to eq true
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should empty cells' do
|
26
|
+
@game.board.fill_cell(0, X)
|
27
|
+
expect(@game.board.empty_cells.size).to eq 8
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should even cells' do
|
31
|
+
expect(@game.board.even_cells.size).to eq 5
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should emty cell' do
|
35
|
+
@game.board.fill_cell(0, X)
|
36
|
+
expect(@game.board.reset_cell(0)).to eq DASH
|
37
|
+
end
|
38
|
+
end
|
data/spec/game_spec.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ttt-cli'
|
4
|
+
|
5
|
+
include Emoji
|
6
|
+
|
7
|
+
describe Game do
|
8
|
+
it 'singleplayer game mode' do
|
9
|
+
Game.set_game_mode(:singleplayer)
|
10
|
+
game = Game.new
|
11
|
+
expect(game.game_mode).to eq :singleplayer
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'hotseat game mode' do
|
15
|
+
Game.set_game_mode(:hotseat)
|
16
|
+
game = Game.new
|
17
|
+
expect(game.game_mode).to eq :hotseat
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'observer game mode' do
|
21
|
+
Game.set_game_mode(:observer)
|
22
|
+
game = Game.new
|
23
|
+
expect(game.game_mode).to eq :observer
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'players for singleplayer mode' do
|
27
|
+
Game.set_game_mode(:singleplayer)
|
28
|
+
game = Game.new
|
29
|
+
game.update_players!
|
30
|
+
|
31
|
+
first_player = game.first_player.class
|
32
|
+
second_player = game.second_player.class
|
33
|
+
current_player = game.current_player.class
|
34
|
+
|
35
|
+
expect(first_player).to eq Players::Human
|
36
|
+
expect(second_player).to eq Players::Computer
|
37
|
+
expect(current_player).to eq Players::Human
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'players for hotseat mode' do
|
41
|
+
Game.set_game_mode(:hotseat)
|
42
|
+
game = Game.new
|
43
|
+
game.update_players!
|
44
|
+
|
45
|
+
first_player = game.first_player.class
|
46
|
+
second_player = game.second_player.class
|
47
|
+
current_player = game.current_player.class
|
48
|
+
|
49
|
+
expect(first_player).to eq Players::Human
|
50
|
+
expect(second_player).to eq Players::Human
|
51
|
+
expect(current_player).to eq Players::Human
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'players for observer mode' do
|
55
|
+
Game.set_game_mode(:observer)
|
56
|
+
game = Game.new
|
57
|
+
game.update_players!
|
58
|
+
|
59
|
+
first_player = game.first_player.class
|
60
|
+
second_player = game.second_player.class
|
61
|
+
current_player = game.current_player.class
|
62
|
+
|
63
|
+
expect(first_player).to eq Players::Computer
|
64
|
+
expect(second_player).to eq Players::Computer
|
65
|
+
expect(current_player).to eq Players::Computer
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'easy level' do
|
69
|
+
Game.set_game_mode(:singleplayer)
|
70
|
+
game = Game.new(RandomAI)
|
71
|
+
expect(game.difficulty).to eq RandomAI
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'medium level' do
|
75
|
+
Game.set_game_mode(:singleplayer)
|
76
|
+
game = Game.new(MediumAI)
|
77
|
+
expect(game.difficulty).to eq MediumAI
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'hard level' do
|
81
|
+
Game.set_game_mode(:singleplayer)
|
82
|
+
game = Game.new(HardAI)
|
83
|
+
expect(game.difficulty).to eq HardAI
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'human goes first' do
|
87
|
+
Game.set_game_mode(:singleplayer)
|
88
|
+
game = Game.new(HardAI)
|
89
|
+
game.update_players!
|
90
|
+
game.set_players_tokens('X')
|
91
|
+
game.who_goes_first
|
92
|
+
expect(game.current_player.token).to eq game.first_player.token
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'computer goes first' do
|
96
|
+
Game.set_game_mode(:singleplayer)
|
97
|
+
game = Game.new(HardAI)
|
98
|
+
game.update_players!
|
99
|
+
game.set_players_tokens('O')
|
100
|
+
game.who_goes_first
|
101
|
+
expect(game.current_player.token).to eq game.second_player.token
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'switch players' do
|
105
|
+
Game.set_game_mode(:singleplayer)
|
106
|
+
|
107
|
+
game = Game.new(RandomAI)
|
108
|
+
first_player = Players::Computer.new(token: X, game: game)
|
109
|
+
game.first_player = first_player
|
110
|
+
game.current_player = first_player
|
111
|
+
|
112
|
+
# проверка переключения игроков
|
113
|
+
2.times do
|
114
|
+
game.current_player.make_move(game.board)
|
115
|
+
game.switch_player
|
116
|
+
end
|
117
|
+
|
118
|
+
expect(game.current_player.token).to eq X
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'check draw' do
|
122
|
+
Game.set_game_mode(:singleplayer)
|
123
|
+
game = Game.new(RandomAI)
|
124
|
+
|
125
|
+
cell = [X, O, X, X, X, O, O, X, O]
|
126
|
+
game.board.cells = cell
|
127
|
+
|
128
|
+
expect(game.draw?).to eq true
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'check win' do
|
132
|
+
Game.set_game_mode(:singleplayer)
|
133
|
+
game = Game.new(RandomAI)
|
134
|
+
|
135
|
+
cell = [X, O, O, X, O, O, X, X, X]
|
136
|
+
game.board.cells = cell
|
137
|
+
game.judge.board = cell
|
138
|
+
|
139
|
+
expect(game.won?).to eq true
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'check counter win' do
|
143
|
+
Game.set_game_mode(:singleplayer)
|
144
|
+
game = Game.new(RandomAI)
|
145
|
+
|
146
|
+
cell = [X, O, O, X, O, O, X, X, X]
|
147
|
+
game.board.cells = cell
|
148
|
+
game.judge.board = cell
|
149
|
+
|
150
|
+
10.times { game.increase_counter }
|
151
|
+
expect(game.wins).to eq 10
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'check counter draws' do
|
155
|
+
Game.set_game_mode(:singleplayer)
|
156
|
+
game = Game.new(RandomAI)
|
157
|
+
|
158
|
+
cell = [X, O, X, X, X, O, O, X, O]
|
159
|
+
game.board.cells = cell
|
160
|
+
|
161
|
+
5.times { game.increase_counter }
|
162
|
+
expect(game.draws).to eq 5
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'check counter losses' do
|
166
|
+
Game.set_game_mode(:singleplayer)
|
167
|
+
game = Game.new(RandomAI)
|
168
|
+
|
169
|
+
cell = [X, DASH, X, DASH, X, DASH, O, O, O]
|
170
|
+
game.board.cells = cell
|
171
|
+
game.judge.board = cell
|
172
|
+
|
173
|
+
8.times { game.increase_counter }
|
174
|
+
expect(game.losses).to eq 8
|
175
|
+
end
|
176
|
+
end
|
data/spec/judge_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ttt-cli'
|
4
|
+
|
5
|
+
include Emoji
|
6
|
+
|
7
|
+
describe Judge do
|
8
|
+
before { @game = Game.new }
|
9
|
+
|
10
|
+
it 'combo check' do
|
11
|
+
board = [X, O, DASH, DASH, X, DASH, DASH, O, X]
|
12
|
+
@game.board.cells = board
|
13
|
+
@game.judge.board = board
|
14
|
+
|
15
|
+
player = @game.current_player
|
16
|
+
expect(@game.judge.is_combo?(player)).to eq true
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
6
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
7
|
+
# files.
|
8
|
+
#
|
9
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
10
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
11
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
12
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
13
|
+
# a separate helper file that requires the additional dependencies and performs
|
14
|
+
# the additional setup, and require it from the spec files that actually need
|
15
|
+
# it.
|
16
|
+
#
|
17
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
18
|
+
RSpec.configure do |config|
|
19
|
+
# rspec-expectations config goes here. You can use an alternate
|
20
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
21
|
+
# assertions if you prefer.
|
22
|
+
config.expect_with :rspec do |expectations|
|
23
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
24
|
+
# and `failure_message` of custom matchers include text for helper methods
|
25
|
+
# defined using `chain`, e.g.:
|
26
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
27
|
+
# # => "be bigger than 2 and smaller than 4"
|
28
|
+
# ...rather than:
|
29
|
+
# # => "be bigger than 2"
|
30
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
34
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
35
|
+
config.mock_with :rspec do |mocks|
|
36
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
37
|
+
# a real object. This is generally recommended, and will default to
|
38
|
+
# `true` in RSpec 4.
|
39
|
+
mocks.verify_partial_doubles = true
|
40
|
+
end
|
41
|
+
|
42
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
43
|
+
# have no way to turn it off -- the option exists only for backwards
|
44
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
45
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
46
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
47
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
48
|
+
|
49
|
+
# The settings below are suggested to provide a good initial experience
|
50
|
+
# with RSpec, but feel free to customize to your heart's content.
|
51
|
+
# # This allows you to limit a spec run to individual examples or groups
|
52
|
+
# # you care about by tagging them with `:focus` metadata. When nothing
|
53
|
+
# # is tagged with `:focus`, all examples get run. RSpec also provides
|
54
|
+
# # aliases for `it`, `describe`, and `context` that include `:focus`
|
55
|
+
# # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
56
|
+
# config.filter_run_when_matching :focus
|
57
|
+
#
|
58
|
+
# # Allows RSpec to persist some state between runs in order to support
|
59
|
+
# # the `--only-failures` and `--next-failure` CLI options. We recommend
|
60
|
+
# # you configure your source control system to ignore this file.
|
61
|
+
# config.example_status_persistence_file_path = "spec/examples.txt"
|
62
|
+
#
|
63
|
+
# # Limits the available syntax to the non-monkey patched syntax that is
|
64
|
+
# # recommended. For more details, see:
|
65
|
+
# # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
66
|
+
# # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
67
|
+
# # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
68
|
+
# config.disable_monkey_patching!
|
69
|
+
#
|
70
|
+
# # This setting enables warnings. It's recommended, but in some cases may
|
71
|
+
# # be too noisy due to issues in dependencies.
|
72
|
+
# config.warnings = true
|
73
|
+
#
|
74
|
+
# # Many RSpec users commonly either run the entire suite or an individual
|
75
|
+
# # file, and it's useful to allow more verbose output when running an
|
76
|
+
# # individual spec file.
|
77
|
+
# if config.files_to_run.one?
|
78
|
+
# # Use the documentation formatter for detailed output,
|
79
|
+
# # unless a formatter has already been configured
|
80
|
+
# # (e.g. via a command-line flag).
|
81
|
+
# config.default_formatter = "doc"
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# # Print the 10 slowest examples and example groups at the
|
85
|
+
# # end of the spec run, to help surface which specs are running
|
86
|
+
# # particularly slow.
|
87
|
+
# config.profile_examples = 10
|
88
|
+
#
|
89
|
+
# # Run specs in random order to surface order dependencies. If you find an
|
90
|
+
# # order dependency and want to debug it, you can fix the order by providing
|
91
|
+
# # the seed, which is printed after each run.
|
92
|
+
# # --seed 1234
|
93
|
+
# config.order = :random
|
94
|
+
#
|
95
|
+
# # Seed global randomization in this process using the `--seed` CLI option.
|
96
|
+
# # Setting this allows you to use `--seed` to deterministically reproduce
|
97
|
+
# # test failures related to randomization by passing the same `--seed` value
|
98
|
+
# # as the one that triggered the failure.
|
99
|
+
# Kernel.srand config.seed
|
100
|
+
end
|
metadata
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ttt-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- dionixs
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.1.2
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.12.2
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.12.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 13.0.1
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '13.0'
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 13.0.1
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.9'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.9'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rainbow
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.0'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: tty-prompt
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 0.21.0
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 0.21.0
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: tty-table
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.11.0
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 0.11.0
|
117
|
+
description:
|
118
|
+
email:
|
119
|
+
- dionixs@tutanota.com
|
120
|
+
executables:
|
121
|
+
- ttt-cli
|
122
|
+
extensions: []
|
123
|
+
extra_rdoc_files: []
|
124
|
+
files:
|
125
|
+
- LICENSE
|
126
|
+
- README.md
|
127
|
+
- bin/console
|
128
|
+
- bin/setup
|
129
|
+
- bin/ttt-cli
|
130
|
+
- lib/ttt-cli.rb
|
131
|
+
- lib/ttt-cli/ai.rb
|
132
|
+
- lib/ttt-cli/board.rb
|
133
|
+
- lib/ttt-cli/display.rb
|
134
|
+
- lib/ttt-cli/emoji.rb
|
135
|
+
- lib/ttt-cli/engine.rb
|
136
|
+
- lib/ttt-cli/game.rb
|
137
|
+
- lib/ttt-cli/judge.rb
|
138
|
+
- lib/ttt-cli/player.rb
|
139
|
+
- lib/ttt-cli/players/computer.rb
|
140
|
+
- lib/ttt-cli/players/human.rb
|
141
|
+
- lib/ttt-cli/strategies/hard_ai.rb
|
142
|
+
- lib/ttt-cli/strategies/medium_ai.rb
|
143
|
+
- lib/ttt-cli/strategies/random_ai.rb
|
144
|
+
- lib/ttt-cli/version.rb
|
145
|
+
- spec/ai_spec.rb
|
146
|
+
- spec/board_spec.rb
|
147
|
+
- spec/game_spec.rb
|
148
|
+
- spec/judge_spec.rb
|
149
|
+
- spec/spec_helper.rb
|
150
|
+
homepage: https://github.com/dionixs/ttt-cli
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata:
|
154
|
+
allowed_push_host: https://rubygems.org
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
require_paths:
|
158
|
+
- lib
|
159
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: 2.7.0
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubygems_version: 3.1.2
|
171
|
+
signing_key:
|
172
|
+
specification_version: 4
|
173
|
+
summary: Консольная реализация игры "Крестики-нолики"
|
174
|
+
test_files: []
|