summaryrefslogtreecommitdiffhomepage
path: root/samples/06_save_load/01_save_load_game/app/main.rb
blob: cc5277085a4821e3bb05cbc65c08231d6d73d76c (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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
=begin

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

 - Symbol (:): Ruby object with a name and an internal ID. Symbols are useful
   because with a given symbol name, you can refer to the same object throughout
   a Ruby program.

   In this sample app, we're using symbols for our buttons. We have buttons that
   light fires, save, load, etc. Each of these buttons has a distinct symbol like
   :light_fire, :save_game, :load_game, etc.

 - to_sym: Returns the symbol corresponding to the given string; creates the symbol
   if it does not already exist.
   For example,
   'car'.to_sym
   would return the symbol :car.

 - last: Returns the last element of an array.

 Reminders:

 - num1.lesser(num2): finds the lower value of the given options.
   For example, in the statement
   a = 4.lesser(3)
   3 has a lower value than 4, which means that the value of a would be set to 3,
   but if the statement had been
   a = 4.lesser(5)
   4 has a lower value than 5, which means that the value of a would be set to 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

 - 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.outputs.labels: An array. Values generate a label.
   Parameters are [X, Y, TEXT, SIZE, ALIGN, RED, GREEN, BLUE, ALPHA, FONT STYLE]

 - ARRAY#inside_rect?: An array with at least two values is considered a point. An array
   with at least four values is considered a rect. The inside_rect? function returns true
   or false depending on if the point is inside the rect.

=end

# This code allows users to perform different tasks, such as saving and loading the game.
# Users also have options to reset the game and light a fire.

class TextedBasedGame

  # Contains methods needed for game to run properly.
  # Increments tick count by 1 each time it runs (60 times in a single second)
  def tick
    default
    show_intro
    state.engine_tick_count += 1
    tick_fire
  end

  # Sets default values.
  # The ||= ensures that a variable's value is only set to the value following the = sign
  # if the value has not already been set before. Intialization happens only in the first frame.
  def default
    state.engine_tick_count ||= 0
    state.active_module     ||= :room
    state.fire_progress     ||= 0
    state.fire_ready_in     ||= 10
    state.previous_fire     ||= :dead
    state.fire              ||= :dead
  end

  def show_intro
    return unless state.engine_tick_count == 0 # return unless the game just started
    set_story_line "awake." # calls set_story_line method, sets to "awake"
  end

  # Sets story line.
  def set_story_line story_line
    state.story_line    = story_line # story line set to value of parameter
    state.active_module = :alert # active module set to alert
  end

  # Clears story line.
  def clear_storyline
    state.active_module = :none # active module set to none
    state.story_line = nil # story line is cleared, set to nil (or empty)
  end

  # Determines fire progress (how close the fire is to being ready to light).
  def tick_fire
    return if state.active_module == :alert # return if active module is alert
    state.fire_progress += 1 # increment fire progress
    # fire_ready_in is 10. The fire_progress is either the current value or 10, whichever has a lower value.
    state.fire_progress = state.fire_progress.lesser(state.fire_ready_in)
  end

  # Sets the value of fire (whether it is dead or roaring), and the story line
  def light_fire
    return unless fire_ready? # returns unless the fire is ready to be lit
    state.fire = :roaring # fire is lit, set to roaring
    state.fire_progress = 0 # the fire progress returns to 0, since the fire has been lit
    if state.fire != state.previous_fire
      set_story_line "the fire is #{state.fire}." # the story line is set using string interpolation
      state.previous_fire = state.fire
    end
  end

  # Checks if the fire is ready to be lit. Returns a boolean value.
  def fire_ready?
    # If fire_progress (value between 0 and 10) is equal to fire_ready_in (value of 10),
    # the fire is ready to be lit.
    state.fire_progress == state.fire_ready_in
  end

  # Divides the value of the fire_progress variable by 10 to determine how close the user is to
  # being able to light a fire.
  def light_fire_progress
    state.fire_progress.fdiv(10) # float division
  end

  # Defines fire as the state.fire variable.
  def fire
    state.fire
  end

  # Sets the title of the room.
  def room_title
    return "a room that is dark" if state.fire == :dead # room is dark if the fire is dead
    return "a room that is lit" # room is lit if the fire is not dead
  end

  # Sets the active_module to room.
  def go_to_room
    state.active_module = :room
  end

  # Defines active_module as the state.active_module variable.
  def active_module
    state.active_module
  end

  # Defines story_line as the state.story_line variable.
  def story_line
    state.story_line
  end

  # Update every 60 frames (or every second)
  def should_tick?
    state.tick_count.mod_zero?(60)
  end

  # Sets the value of the game state provider.
  def initialize game_state_provider
    @game_state_provider = game_state_provider
  end

  # Defines the game state.
  # Any variable prefixed with an @ symbol is an instance variable.
  def state
    @game_state_provider.state
  end

  # Saves the state of the game in a text file called game_state.txt.
  def save
    $gtk.serialize_state('game_state.txt', state)
  end

  # Loads the game state from the game_state.txt text file.
  # If the load is unsuccessful, the user is informed since the story line indicates the failure.
  def load
    parsed_state = $gtk.deserialize_state('game_state.txt')
    if !parsed_state
      set_story_line "no game to load. press save first."
    else
      $gtk.args.state = parsed_state
    end
  end

  # Resets the game.
  def reset
    $gtk.reset
  end
end

class TextedBasedGamePresenter
  attr_accessor :state, :outputs, :inputs

  # Creates empty collection called highlights.
  # Calls methods necessary to run the game.
  def tick
    state.layout.highlights ||= []
    game.tick if game.should_tick?
    render
    process_input
  end

  # Outputs a label of the tick count (passage of time) and calls all render methods.
  def render
    outputs.labels << [10, 30, state.tick_count]
    render_alert
    render_room
    render_highlights
  end

  # Outputs a label onto the screen that shows the story line, and also outputs a "close" button.
  def render_alert
    return unless game.active_module == :alert

    outputs.labels << [640, 480, game.story_line, 5, 1]  # outputs story line label
    outputs.primitives << button(:alert_dismiss, 490, 380, "close")  # positions "close" button under story line
  end

  def render_room
    return unless game.active_module == :room
    outputs.labels << [640, 700, game.room_title, 4, 1] # outputs room title label at top of screen

    # The parameters for these outputs are (symbol, x, y, text, value/percentage) and each has a y value
    # that positions it 60 pixels lower than the previous output.

    # outputs the light_fire_progress bar, uses light_fire_progress for its percentage (which changes bar's appearance)
    outputs.primitives << progress_bar(:light_fire, 490, 600, "light fire", game.light_fire_progress)
    outputs.primitives << button(       :save_game, 490, 540, "save") # outputs save button
    outputs.primitives << button(       :load_game, 490, 480, "load") # outputs load button
    outputs.primitives << button(      :reset_game, 490, 420, "reset") # outputs reset button
    outputs.labels << [640, 30, "the fire is #{game.fire}", 0, 1] # outputs fire label at bottom of screen
  end

  # Outputs a collection of highlights using an array to set their values, and also rejects certain values from the collection.
  def render_highlights
    state.layout.highlights.each do |h| # for each highlight in the collection
        h.lifetime -= 1 # decrease the value of its lifetime
      end

      outputs.solids << state.layout.highlights.map do |h| # outputs highlights collection
        [h.x, h.y, h.w, h.h, h.color, 255 * h.lifetime / h.max_lifetime] # sets definition for each highlight
        # transparency changes; divide lifetime by max_lifetime, multiply result by 255
      end

      # reject highlights from collection that have no remaining lifetime
      state.layout.highlights = state.layout.highlights.reject { |h| h.lifetime <= 0 }
  end

  # Checks whether or not a button was clicked.
  # Returns a boolean value.
  def process_input
    button = button_clicked? # calls button_clicked? method
  end

  # Returns a boolean value.
  # Finds the button that was clicked from the button list and determines what method to call.
  # Adds a highlight to the highlights collection.
  def button_clicked?
    return nil unless click_pos # return nil unless click_pos holds coordinates of mouse click
      button = @button_list.find do |k, v| # goes through button_list to find button clicked
        click_pos.inside_rect? v[:primitives].last.rect # was the mouse clicked inside the rect of button?
      end
      return unless button # return unless a button was clicked
      method_to_call = "#{button[0]}_clicked".to_sym # sets method_to_call to symbol (like :save_game or :load_game)
      if self.respond_to? method_to_call # returns true if self responds to the given method (method actually exists)
        border = button[1][:primitives].last # sets border definition using value of last key in button list hash

        # declares each highlight as a new entity, sets properties
        state.layout.highlights << state.new_entity(:highlight) do |h|
            h.x = border.x
            h.y = border.y
            h.w = border.w
            h.h = border.h
            h.max_lifetime = 10
            h.lifetime = h.max_lifetime
            h.color = [120, 120, 180] # sets color to shade of purple
          end

          self.send method_to_call # invoke method identified by symbol
        else # otherwise, if self doesn't respond to given method
          border = button[1][:primitives].last # sets border definition using value of last key in hash

          # declares each highlight as a new entity, sets properties
          state.layout.highlights << state.new_entity(:highlight) do |h|
            h.x = border.x
            h.y = border.y
            h.w = border.w
            h.h = border.h
            h.max_lifetime = 4 # different max_lifetime than the one set if respond_to? had been true
            h.lifetime = h.max_lifetime
            h.color = [120, 80, 80] # sets color to dark color
          end

          # instructions for users on how to add the missing method_to_call to the code
          puts "It looks like #{method_to_call} doesn't exists on TextedBasedGamePresenter. Please add this method:"
          puts "Just copy the code below and put it in the #{TextedBasedGamePresenter} class definition."
          puts ""
          puts "```"
          puts "class TextedBasedGamePresenter <--- find this class and put the method below in it"
          puts ""
          puts "  def #{method_to_call}"
          puts "    puts 'Yay that worked!'"
          puts "  end"
          puts ""
          puts "end <-- make sure to put the #{method_to_call} method in between the `class` word and the final `end` statement."
          puts "```"
          puts ""
      end
  end

  # Returns the position of the mouse when it is clicked.
  def click_pos
    return nil unless inputs.mouse.click # returns nil unless the mouse was clicked
    return inputs.mouse.click.point # returns location of mouse click (coordinates)
  end

  # Creates buttons for the button_list and sets their values using a hash (uses symbols as keys)
  def button id, x, y, text
    @button_list[id] ||= { # assigns values to hash keys
      id: id,
      text: text,
      primitives: [
        [x + 10, y + 30, text, 2, 0].label, # positions label inside border
        [x, y, 300, 50].border,             # sets definition of border
      ]
    }

    @button_list[id][:primitives] # returns label and border for buttons
  end

  # Creates a progress bar (used for lighting the fire) and sets its values.
  def progress_bar id, x, y, text, percentage
    @button_list[id] = { # assigns values to hash keys
      id: id,
      text: text,
      primitives: [
        [x, y, 300, 50, 100, 100, 100].solid, # sets definition for solid (which fills the bar with gray)
        [x + 10, y + 30, text, 2, 0].label, # sets definition for label, positions inside border
        [x, y, 300, 50].border, # sets definition of border
      ]
    }

    # Fills progress bar based on percentage. If the fire was ready to be lit (100%) and we multiplied by
    # 100, only 1/3 of the bar would only be filled in. 200 would cause only 2/3 to be filled in.
    @button_list[id][:primitives][0][2] = 300 * percentage
    @button_list[id][:primitives]
  end

  # Defines the game.
  def game
    @game
  end

  # Initalizes the game and creates an empty list of buttons.
  def initialize
    @game = TextedBasedGame.new self
    @button_list ||= {}
  end

  # Clears the storyline and takes the user to the room.
  def alert_dismiss_clicked
    game.clear_storyline
    game.go_to_room
  end

  # Lights the fire when the user clicks the "light fire" option.
  def light_fire_clicked
    game.light_fire
  end

  # Saves the game when the user clicks the "save" option.
  def save_game_clicked
    game.save
  end

  # Resets the game when the user clicks the "reset" option.
  def reset_game_clicked
    game.reset
  end

  # Loads the game when the user clicks the "load" option.
  def load_game_clicked
    game.load
  end
end

$text_based_rpg = TextedBasedGamePresenter.new

def tick args
  $text_based_rpg.state = args.state
  $text_based_rpg.outputs = args.outputs
  $text_based_rpg.inputs = args.inputs
  $text_based_rpg.tick
end