summaryrefslogtreecommitdiffhomepage
path: root/samples/05_mouse/01_mouse_click/app/main.rb
blob: c1a6e290ae1ab32d08acbbbbd8218109e44b3848 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
=begin

 APIs listing that haven't been encountered in previous sample apps:

 - product: Returns an array of all combinations of elements from all arrays.

   For example, [1,2].product([1,2]) would return the following array...
   [[1,1], [1,2], [2,1], [2,2]]
   More than two arrays can be given to product and it will still work,
   such as [1,2].product([1,2],[3,4]). What would product return in this case?

   Answer:
   [[1,1,3],[1,1,4],[1,2,3],[1,2,4],[2,1,3],[2,1,4],[2,2,3],[2,2,4]]

 - num1.fdiv(num2): Returns the float division (will have a decimal) of the two given numbers.
   For example, 5.fdiv(2) = 2.5 and 5.fdiv(5) = 1.0

 - yield: Allows you to call a method with a code block and yield to that block.

 Reminders:

 - ARRAY#inside_rect?: Returns true or false depending on if the point is inside the rect.

 - String interpolation: Uses #{} syntax; everything between the #{ and the } is evaluated
   as Ruby code, and the placeholder is replaced with its corresponding value or result.

 - args.inputs.mouse.click: This property will be set if the mouse was clicked.

 - Ternary operator (?): Will evaluate a statement (just like an if statement)
   and perform an action if the result is true or another action if it is false.

 - reject: Removes elements from a collection if they meet certain requirements.

 - args.outputs.borders: An array. The values generate a border.
   The parameters are [X, Y, WIDTH, HEIGHT, RED, GREEN, BLUE]

 - args.outputs.labels: An array. The values generate a label.
   The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]

=end

# This sample app is a classic game of Tic Tac Toe.

class TicTacToe
  attr_accessor :_, :state, :outputs, :inputs, :grid, :gtk

  # Starts the game with player x's turn and creates an array (to_a) for space combinations.
  # Calls methods necessary for the game to run properly.
  def tick
    state.current_turn ||= :x
    state.space_combinations = [-1, 0, 1].product([-1, 0, 1]).to_a
    render_board
    input_board
  end

  # Uses borders to create grid squares for the game's board. Also outputs the game pieces using labels.
  def render_board
    square_size = 80

    # Positions the game's board in the center of the screen.
    # Try removing what follows grid.w_half or grid.h_half and see how the position changes!
    board_left = grid.w_half - square_size * 1.5
    board_top  = grid.h_half - square_size * 1.5

    # At first glance, the add(1) looks pretty trivial. But if you remove it,
    # you'll see that the positioning of the board would be skewed without it!
    # Or if you put 2 in the parenthesis, the pieces will be placed in the wrong squares
    # due to the change in board placement.
    outputs.borders << all_spaces do |x, y, space| # outputs borders for all board spaces
      space.border ||= [
        board_left + x.add(1) * square_size, # space.border is initialized using this definition
        board_top  + y.add(1) * square_size,
        square_size,
        square_size
      ]
    end

    # Again, the calculations ensure that the piece is placed in the center of the grid square.
    # Remove the '- 20' and the piece will be placed at the top of the grid square instead of the center.
    outputs.labels << filled_spaces do |x, y, space| # put label in each filled space of board
          label board_left + x.add(1) * square_size + square_size.fdiv(2),
          board_top  + y.add(1) * square_size + square_size - 20,
          space.piece # text of label, either "x" or "o"
    end

    # Uses a label to output whether x or o won, or if a draw occurred.
    # If the game is ongoing, a label shows whose turn it currently is.
    outputs.labels << if state.x_won
                        label grid.w_half, grid.top - 80, "x won" # the '-80' positions the label 80 pixels lower than top
                      elsif state.o_won
                        label grid.w_half, grid.top - 80, "o won" # grid.w_half positions the label in the center horizontally
                      elsif state.draw
                        label grid.w_half, grid.top - 80, "a draw"
                      else # if no one won and the game is ongoing
                        label grid.w_half, grid.top - 80, "turn: #{state.current_turn}"
                      end
  end

  # Calls the methods responsible for handling user input and determining the winner.
  # Does nothing unless the mouse is clicked.
  def input_board
    return unless inputs.mouse.click
    input_place_piece
    input_restart_game
    determine_winner
  end

  # Handles user input for placing pieces on the board.
  def input_place_piece
    return if state.game_over

    # Checks to find the space that the mouse was clicked inside of, and makes sure the space does not already
    # have a piece in it.
    __, __, space = all_spaces.find do |__, __, space|
      inputs.mouse.click.point.inside_rect?(space.border) && !space.piece
    end

    # The piece that goes into the space belongs to the player whose turn it currently is.
    return unless space
    space.piece = state.current_turn

    # This ternary operator statement allows us to change the current player's turn.
    # If it is currently x's turn, it becomes o's turn. If it is not x's turn, it become's x's turn.
    state.current_turn = state.current_turn == :x ? :o : :x
  end

  # Resets the game.
  def input_restart_game
    return unless state.game_over
    gtk.reset
  end

  # Checks if x or o won the game.
  # If neither player wins and all nine squares are filled, a draw happens.
  # Once a player is chosen as the winner or a draw happens, the game is over.
  def determine_winner
    state.x_won = won? :x # evaluates to either true or false (boolean values)
    state.o_won = won? :o
    state.draw = true if filled_spaces.length == 9 && !state.x_won && !state.o_won
    state.game_over = state.x_won || state.o_won || state.draw
  end

  # Determines if a player won by checking if there is a horizontal match or vertical match.
  # Horizontal_match and vertical_match have boolean values. If either is true, the game has been won.
  def won? piece
    # performs action on all space combinations
    won = [[-1, 0, 1]].product([-1, 0, 1]).map do |xs, y|

      # Checks if the 3 grid spaces with the same y value (or same row) and
      # x values that are next to each other have pieces that belong to the same player.
      # Remember, the value of piece is equal to the current turn (which is the player).
      horizontal_match = state.spaces[xs[0]][y].piece == piece &&
                         state.spaces[xs[1]][y].piece == piece &&
                         state.spaces[xs[2]][y].piece == piece

      # Checks if the 3 grid spaces with the same x value (or same column) and
      # y values that are next to each other have pieces that belong to the same player.
      # The && represents an "and" statement: if even one part of the statement is false,
      # the entire statement evaluates to false.
      vertical_match = state.spaces[y][xs[0]].piece == piece &&
                       state.spaces[y][xs[1]].piece == piece &&
                       state.spaces[y][xs[2]].piece == piece

      horizontal_match || vertical_match # if either is true, true is returned
    end

    # Sees if there is a diagonal match, starting from the bottom left and ending at the top right.
    # Is added to won regardless of whether the statement is true or false.
    won << (state.spaces[-1][-1].piece == piece && # bottom left
            state.spaces[ 0][ 0].piece == piece && # center
            state.spaces[ 1][ 1].piece == piece)   # top right

    # Sees if there is a diagonal match, starting at the bottom right and ending at the top left
    # and is added to won.
    won << (state.spaces[ 1][-1].piece == piece && # bottom right
            state.spaces[ 0][ 0].piece == piece && # center
            state.spaces[-1][ 1].piece == piece)   # top left

    # Any false statements (meaning false diagonal matches) are rejected from won
    won.reject_false.any?
  end

  # Defines filled spaces on the board by rejecting all spaces that do not have game pieces in them.
  # The ! before a statement means "not". For example, we are rejecting any space combinations that do
  # NOT have pieces in them.
  def filled_spaces
    state.space_combinations
      .reject { |x, y| !state.spaces[x][y].piece } # reject spaces with no pieces in them
      .map do |x, y|
        if block_given?
          yield x, y, state.spaces[x][y]
        else
          [x, y, state.spaces[x][y]] # sets definition of space
        end
    end
  end

  # Defines all spaces on the board.
  def all_spaces
    if !block_given?
      state.space_combinations.map do |x, y|
        [x, y, state.spaces[x][y]] # sets definition of space
      end
    else # if a block is given (block_given? is true)
      state.space_combinations.map do |x, y|
        yield x, y, state.spaces[x][y] # yield if a block is given
      end
    end
  end

  # Sets values for a label, such as the position, value, size, alignment, and color.
  def label x, y, value
    [x, y + 10, value, 20, 1, 0, 0, 0]
  end
end

$tic_tac_toe = TicTacToe.new

def tick args
  $tic_tac_toe._       = args
  $tic_tac_toe.state   = args.state
  $tic_tac_toe.outputs = args.outputs
  $tic_tac_toe.inputs  = args.inputs
  $tic_tac_toe.grid    = args.grid
  $tic_tac_toe.gtk     = args.gtk
  $tic_tac_toe.tick
  tick_instructions args, "Sample app shows how to work with mouse clicks."
end

def tick_instructions args, text, y = 715
  return if args.state.key_event_occurred
  if args.inputs.mouse.click ||
     args.inputs.keyboard.directional_vector ||
     args.inputs.keyboard.key_down.enter ||
     args.inputs.keyboard.key_down.escape
    args.state.key_event_occurred = true
  end

  args.outputs.debug << [0, y - 50, 1280, 60].solid
  args.outputs.debug << [640, y, text, 1, 1, 255, 255, 255].label
  args.outputs.debug << [640, y - 25, "(click to dismiss instructions)" , -2, 1, 255, 255, 255].label
end