This past week, we've been learning to make Rails applications using forms at Flatiron School. It didn't take long for me to grow sick of looking at forms after forms and I wanted to see if I could make it more fun. So to reinforce what I've learned, I decided to make a simple tic-tac-toe game using Rails forms.
Models
This simple game just needed two models; Game and Turn. A game has many turns, and a turn belongs to a game. The game would keep track of all of the turns (up to 9) and the winner, if any. The turn would keep track of which mark it is (X or O) as well as where it is placed on the tic-tac-toe board.
Views
The views would follow the RESTful pattern of the actions :index
for listing all games, :show
for the current state/result of a game, :new
for starting a new game with some parameters (single or two players), :create
for creating the new game, :edit
to POST the user's move, and :update
for updating the game state.
Controller
The controller (with methods defined in the game model and game helper) handles the flow to view different games and to start or resume a game. For the game, it serves the :edit view as long as the game is in progress (game loop), then the :show view for the conclusion of the game.
Less Form-like, More Game-like
Routes
I wanted to customize the route urls to mask the fact that the user is interacting with forms while still keeping it as RESTful.
# routes.rb
get '/games/:id/play', to: 'games#edit', as: 'game_play'
patch '/games/:id/play', to: 'games#update', as: 'update_game_play'
The routes and corresponding views are set up as follows:
'/' → Main Menu
'/games/' → List of all played games
'/games/new' → Form to create new game (single player or two players)
'/games/:id/play' → Form to update game with user move (re-renders)
'/games/:id' → Current state/result of game with turn history
Forms
Now, for the forms. I would have to POST the move a user makes and re-render with an updated game board, and I've learned to do so with submit buttons with Rails forms. So I made each of the 9 spots on the board submit buttons and hidden fields for params that would POST its board_index, and with CSS made it so that they don't look like the generic form buttons.
Another cool feature I was able to add to the game was that since I had access to the array of turns from the game in the order they were created, I could display a "turn history" using a cumulative array of arrays representing the game board state at each turn.
Game Logic
For a simple game like tic-tac-toe, the core components of the game are the check to see if win conditions are met and the AI logic. This was done by comparing an array of all winning combos (by board index) and comparing that array to the game state array for X's and O's respectively. This is what the AI would use to place a mark for a winning move when there are two out of three for a winning combo, or to block a potential player's winning combo. Otherwise, the AI will try to prioritize taking corners of the board.
module GamesHelper
@@win_conditions = [
# Rows
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
# Columns
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
# Diagonals
[0, 4, 8],
[2, 4, 6]
]
# Returns an array with 'X' or 'O' and string of winning combo if there is a winner
# or 'TIE' and an empty string if game ended with no winners
# or nil if game has not yet ended
def check_for_win(game)
return nil if game.turns.size < 5
xs = game.turns.where(mark: 'X').map {|turn| turn.board_index }
os = game.turns.where(mark: 'O').map {|turn| turn.board_index }
@@win_conditions.each do |win|
# Check if xs or os contain indices combo that meet win condition
if (win-xs).empty?
return ["X", win.join(' ')]
elsif (win-os).empty?
return ["O",win.join(' ')]
end
end
if game.turns.size > 8
return ["TIE",""]
else
return nil
end
end
# AI Logic
# 1. If there's two in a row, get the third for the win
# 2. If player has two in a row, block the third
# 3. Get a corner
# 4. Get the center
def do_move(game, mark)
player = mark == 'X' ? 'O' : 'X'
player_marks = game.turns.where(mark: player).map {|turn| turn.board_index}
marks = game.turns.where(mark: mark).map {|turn| turn.board_index}
i = nil
# Look for winning move
@@win_conditions.each do |win|
if (win-marks).size == 1 && !game.board[(win-marks).first]
i = (win-marks).first
end
end
if !i
# Block any potential wins for player
@@win_conditions.each do |win|
if (win-player_marks).size == 1 && !game.board[(win-player_marks).first]
i = (win-player_marks).first
end
end
end
if !i
# Prioritize corners
if !game.board[0] || !game.board[2] || !game.board[6] || !game.board[8]
i = [0, 2, 6, 8].sample
while game.board[i]
i = [0, 2, 6, 8].sample
end
# Else prioritize center
elsif !game.board[4]
i = 4
end
end
# If all else fails just pick a random open spot
if !i
i = [1, 3, 5, 7].sample
while game.board[i]
i = [1, 3, 5, 7].sample
end
end
game.turns.build(mark: mark, board_index: i).save
end
end
This method utilizes some really neat Ruby array operations that allows you to "subtract" two arrays to get just the elements that are exclusive. So if the difference between an array of a winning combo (for example, [0, 1, 2] for the top row) and an array of the X's on the board results in an empty array, that would indicate that the player has 0, 1, 2 on the board.
Overall, this was a fun side project that gave me an escape from the mundane world of forms using form_for
and allowed me to experiment with customization of routes and to test the boundaries of the RESTful pattern.
Following the MVC pattern made working on the project a lot smoother as it made structuring the project very easy and made the roles for different parts of the application very clear, and I've definitely grown to appreciate this pattern.
This project is available at https://github.com/bbpak/rails-forms-tic-tac-toe
Discussion
niiiiice! super clever, and it looks great! very curious to talk more with you about how you approached the AI logic! :)
This is great! If you have a passion for video games it might be worth pursuing learning Unity or the Unreal Engine.
I'm definitely thinking about Unity! I was able to learn C# through making mods for Stardew Valley.
Wow, this is amazing. It looks like you are a genius in this field. I also want to be like you because I love video games. Now a days I am working for a writing website. If you have any work for me you can contact us at paymetodoyourhomework.com scam and we will help you with your problem. You can get tips from us or can reads reviews about many essay writing works.
Very good use of function with class I not down it. Because it is a very important function. I am a web developer in the latest news of Philadelphia so sometimes I need that type of the codes.