- clone the repo and install the Ruby gems
$ git clone https://github.com/elenamorton/cli-tic-tac-toe.git
$ cd cli-tic-tac-toe
$ bundle install
- run tests
$ rspec
- test coverage
- run the CLI application.
We have to make sure the script is marked as executable. If it is not, please run the command
chmod +x app.rb
$ ./app.rb
- ruby 2.3.4p301
- rspec (3.6.0)
- simplecov (0.15.0)
- Allow the user to choose the level of difficulty (“easy” means the computer can easily be beaten, “medium” means it can be beaten but only with a series of intelligent moves, and “hard” means the it is unbeatable).
- Allow the user to choose the game type (human v. human, computer v. computer, human v. computer).
- Allow the user to choose which player goes first.
- Allow the user to choose with what "symbol" the players will mark their selections on the board (traditionally it's "X" and "O")
As a user
So as I can have a nice time
I'd like to be able to play tic-tac-toe game
As a user
So as I can decide on computer play level
I'd like to see the options available
As a user
So as I can make a choice the computer play level
I'd like to be able to select it
As a user
So as I can make a choice about the players
I'd like to be able to select the players type
As a user
So as I can make a choice about the players order
I'd like to be able to select the which players starts first
As a user
So as I can see the players moves on the game board
I'd like to be able to select the players symbols they mark their moves
- The user is providing all the game setup information at the start of game, via console inputs, answering to the questions printed on the screen.
- The user is not choosing the same symbol marker for both players.
- The user is inputting all the moves, each at a time, for any 'Human' player.
The current design contains five classes Game, Board, Scorer, Human
, and Computer
, and an IO module IOlike
.
The design tries to follow SOLID principles, each class having single responsibility (athough game
is more fat), DRY code,
creating loose class dependencies by dependency injections (game
injects scorer
object to each player
objects).
Encapsulation is served by hidding the board
and score
tables inside their own objects, everyone communicating with these objects receiving copies of the tables to use.
Duck-typing is achieved by unifying the human
and computer
players behaviour, publishing a common API method for both: move = @current_player.get_next_move(@valid_moves)
- The main class is the
Game
class, that is containing all the game configuration data, creates all required objects based on game setup data, and handles the game flow. This class is intantiated by the application scriptapp.rb
. - The
Board
class is instantiated when aGame
is started and updated by thegame
object after each player makes a valid move. Theboard
default width is3
, but it can be set bygame
to any other number. Theboard
instance variable is set up an as array of ordered strings, starting'0'
up towidth**2
. Each value string is thei.to_s
array position. The actualboard
is never seen by thegame
, this always receives a copy of theboard
for handling. Theboard
variable instance is updating byboard
object with the symbol a player has used for its move, by calling@board_play.place_marker(marker, spot)
. - The
Scorer
class is handling the game score. It is the instantiated when aGame
is started, and updated by thegame
object after each player makes a valid move in thepost_move_updates
by calling@scorer.calculate_score(spot, marker)
. Thescorer
object is independent on the players symbols. It is only setup one with value+1
and the other with-1
for calculating a move score. Ascore_table
is a hash table with 8 elements for a '3 x 3' grid. It contains a key for each row, coloumn, diagonal, and anti-diagonal such as{:D=>-1, :antiD=>-3, :R0=>0, :R1=>0, :R2=>-1, :C0=>0, :C1=>0, :C2=>-1}
Ascore_table
copy is used by thecomputer
player for calculating its next best move. Although, not fully implemented at this moment, only the basic support is provided. Awin
happens when any absolute value in the@score_table
is equal3.
- The
Human
class is instantiated by thegame
after the user chooses the players type. It can be instantiated once in ahuman v. computer
game, or twice in ahuman v. human
game. Each instance is injected ascorer
dependency for future support of difficulty play levels used by acomputer
player. - The
Computer
class is instantiated by thegame
after the user chooses the players type. It can be instantiated once in ahuman v. computer
game, or twice in acomputer v. computer
game. Each instance is injected ascorer
dependency for calculating the computer best move. - The
IOlike
module is handling all the input/output operations required by thegame
,human
player, orrspec
tests. Additionally, theget_input
message handles gracefully a bad user input, by reprinting the message with expected input until user introduces the correct input.
- Game class instantiation
game = Game.new
game.start_game
- Board class instantiation
@board_play = Board.new(@width)
@board = @board_play.board
- Scorer class instantiation
@scorer = Scorer.new({:width => @width, :x_marker => X_MARKER, :o_marker => O_MARKER})
- Human class instantiation
@players[] = Human.new(outgoing, incoming, {:width => @width, :marker => marker, :scorer => @scorer})
- Computer class instantiation
@players[] = Computer.new({:width => @width, :marker => marker, :scorer => @scorer, :depth => 0})
- Current and opposing players are selected based on user input choice
def setup_players_order
player = get_input("Please, choose which players goes first (1|2)", /\A1|2\z/)
@current_player, @opposing_player = player_1, player_2 if player == '1'
@current_player, @opposing_player = player_2, player_1 if player == '2'
end
The computer difficulty level is not currently implemented in the Computer class. The current implementation is a 'easy' level. The compuer player is practically choosing the next move randomly from a valid moves array. Please, see the 'Future extensions' section for implementation suggestions.
def get_best_move(valid_moves, depth, best_score = {})
score_table_copy = @scorer.score_table.dup
valid_moves.each do |move|
@scorer.calculate_score(score_table_copy, move, @marker)
if @scorer.win?
return move
else
@scorer.calculate_score(score_table_copy, move, @marker)
if @scorer.win?
return move
end
end
end
valid_moves.sample
end
$ ./app.rb
Please, choose a player (human|computer): computer
Please, choose another player (human|computer): computer
Please, choose a player symbol (X|other letter): 7
You've entered 7, which is invalid
Please, choose a player symbol (X|other letter): o
Please, choose the other player symbol (O|other letter): z
Please, choose the difficulty level (easy|medium|hard): easy
Please, choose which players goes first (1|2): 1
0 | 1 | 2
===+===+===
3 | 4 | 5
===+===+===
6 | 7 | 8
0 | 1 | 2
===+===+===
3 | O | 5
===+===+===
6 | 7 | 8
0 | 1 | 2
===+===+===
3 | O | 5
===+===+===
6 | Z | 8
0 | 1 | 2
===+===+===
3 | O | O
===+===+===
6 | Z | 8
Z | 1 | 2
===+===+===
3 | O | O
===+===+===
6 | Z | 8
Z | 1 | O
===+===+===
3 | O | O
===+===+===
6 | Z | 8
Z | 1 | O
===+===+===
Z | O | O
===+===+===
6 | Z | 8
Z | 1 | O
===+===+===
Z | O | O
===+===+===
6 | Z | O
Game over
At present, the following four categories are provided suggestions for improvement.
The current game layout is lacking separation between the players moves, noticed especially when a game computer v.computer
is played.
Z | 1 | O
===+===+===
Z | O | O
===+===+===
6 | Z | 8
Z | 1 | O
===+===+===
Z | O | O
===+===+===
6 | Z | O
This should be done either,
- by introducing a message after the board is displayed, like
Next player move
, that can be done ingame
object, extending thedisplay_board(board_play)
with anoutgoing.puts 'Next player move'
; - or, inside the
board
object, by extending theboard_stringify
, to add a line separator like"+++=+++=+++\n"
, or even a simply empty line"\n"
When the game is over, it would be nice to know how this has finished, if a tie, or a win. For a win, would be good to display the winner.
- we can expand the 'game' before displaying the
outgoing.puts "Game over"
, to check if@scorer.win?
. If true, we identify the player with the absolute score3
and set it in awinner
variable so as we can display"The winner is #{winner}!\n"
, followed by theGame over
message. - if not a win, we can add a check for
@board_play.tie?
. If true, we add the messageThis is a tie!
, then display theGame over
message.
- An alternative to user introducing the game configuration from the console, can be to have the configuration available in a file e.g.
setup.cfg
, each line containing a separate input parameter. The name of the parameter can be at the beginning of the line, to allow easy identification of parameters, and not rely on the lines order, using a YAML, or JSON formats. Theapp
can check if the config file exists. If true, it should pass it to thegame
when instantiating this object. If false, thegame
with get the input from the console. - Other suggestion can be to use a
Config
module usingostruct
.
While trying to implement this feature, I searched the internet and came across the following site An Introduction to Game Tree Algorithms
, that presents a few methods for implementing games with two players with zero sum
, i.e the total amount of 'payoff' is constant. Examples of the methods are: 'the game tree', 'minimax algorithm', 'negamax algorithm', 'alpha-beta pruning and branching', 'the horizon effect'.
The list is not exclusive, other methods do exist.
My current implementation for the score_table
is complying with the 'constant payoff' rule, and I came up with this before reading the above link. I was thinking of a hash to keep the score, and found the 'scheme' of one player score changing with '+1' and the other with '-1'.
This calculation suggests 'any game situation is as good for player A as it is bad for player B, and vice versa'. Additionally, the game is finite. 'A finite game is a game in which every possible combination of moves leads to an end of the game. That is, it is impossible for the game to go on forever!''
The main idea about adding a difficulty level to the computer
player, is to improve its moves until unbeatable. All these algorithm try to 'look forward' a few moves, and each is doing this in a different way.
I plan to add this missing implementation in the near future.