loading...
Cover image for Will It Game?: Rails Forms

Will It Game?: Rails Forms

bouhm profile image Brian Pak Updated on ・5 min read

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.

gameplay

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'

using aliases to make you forget you're in an edit form

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

views

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

check for wins & implement AI logic

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

pic
Editor guide
Collapse
isalevine profile image
Isa Levine

niiiiice! super clever, and it looks great! very curious to talk more with you about how you approached the AI logic! :)

Collapse
jwesorick profile image
Jake Wesorick

This is great! If you have a passion for video games it might be worth pursuing learning Unity or the Unreal Engine.

Collapse
bouhm profile image
Brian Pak Author

I'm definitely thinking about Unity! I was able to learn C# through making mods for Stardew Valley.

Collapse
caus2000 profile image
Caus2000

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.

Collapse
anthonytcarpenter profile image
AnthonyTCarpenter

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.