RSokoban 0.73 → 0.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,256 +1,148 @@
1
- #require 'tk'
2
- #require 'tkextlib/tkimg'
3
-
4
1
  module RSokoban::UI
5
-
6
- # I am an image of the game who knows how to display itself on an array
7
- # of TkLabel items.
8
- # @since 0.73
9
- class Image
10
-
11
- # @param [String] file path of image file
12
- # @param [Array<Array<TkLabel>>] output an array to display myself
13
- def initialize file, output
14
- @box = TkPhotoImage.new('file' => file, 'height' => 0, 'width' => 0)
15
- @output = output
16
- end
17
-
18
- # Display myself at x,y coordinate
19
- def display_at x, y
20
- @output[y][x].configure('image' => @box)
21
- end
22
-
23
- end
24
-
25
- class LevelDialog < TkToplevel
26
- def initialize(root, title)
27
- super(root)
28
- title(title)
29
- minsize(200, 100)
30
- @state = 'CANCEL'
31
- grab
32
-
33
- $spinval = TkVariable.new
34
- @spin = TkSpinbox.new(self) do
35
- textvariable($spinval)
36
- grid('row'=>0, 'column'=>0)
37
- end
38
- @spin.to(999)
39
- @spin.from(1)
40
-
41
- @ok = TkButton.new(self) do
42
- text 'OK'
43
- grid('row'=>1, 'column'=>0)
44
- end
45
- @ok.command { ok_on_clic }
46
-
47
- @cancel = TkButton.new(self) do
48
- text 'Cancel'
49
- grid('row'=>1, 'column'=>1)
50
- end
51
- @cancel.command { cancel_on_clic }
52
-
53
- wait_destroy
54
- end
55
-
56
- def ok_on_clic
57
- @state = 'OK'
58
- destroy
59
- end
60
-
61
- def cancel_on_clic
62
- @state = 'CANCEL'
63
- destroy
64
- end
65
-
66
- def ok?
67
- @state == 'OK'
68
- end
69
-
70
- def value
71
- $spinval.to_i
72
- end
73
- end
74
-
75
- class SetDialog < TkToplevel
76
- def initialize(root, title)
77
- super(root)
78
- title(title)
79
- minsize(200, 100)
80
- @state = 'CANCEL'
81
- grab
82
-
83
- @value = nil
84
- @xsb = get_xsb
85
- $listval = TkVariable.new(@xsb)
86
-
87
- @list = TkListbox.new(self) do
88
- width 20
89
- height 8
90
- listvariable $listval
91
- grid('row'=>0, 'column'=>0)
92
- end
93
-
94
- @ok = TkButton.new(self) do
95
- text 'OK'
96
- grid('row'=>1, 'column'=>0)
97
- end
98
- @ok.command { ok_on_clic }
99
-
100
- @cancel = TkButton.new(self) do
101
- text 'Cancel'
102
- grid('row'=>1, 'column'=>1)
103
- end
104
- @cancel.command { cancel_on_clic }
105
-
106
- wait_destroy
107
- end
108
-
109
- def ok_on_clic
110
- @state = 'OK'
111
- idx = @list.curselection
112
- unless idx.empty?
113
- @value = @xsb[idx[0]]
114
- end
115
- destroy
116
- end
117
-
118
- def cancel_on_clic
119
- @state = 'CANCEL'
120
- destroy
121
- end
122
-
123
- def ok?
124
- @state == 'OK'
125
- end
126
-
127
- def value
128
- @value
129
- end
130
-
131
- def get_xsb
132
- current = Dir.pwd
133
- Dir.chdir $RSOKOBAN_DATA_PATH
134
- ret = Dir.glob '*.xsb'
135
- Dir.chdir current
136
- ret
137
- end
138
- end
139
-
140
- class HelpDialog < TkToplevel
141
- def initialize(root, title)
142
- super(root)
143
- title(title)
144
- minsize(200, 100)
145
-
146
- text = TkText.new(self) do
147
- borderwidth 1
148
- font TkFont.new('times 12 bold')
149
- grid('row' => 0, 'column' => 0)
150
- end
151
-
152
- help=<<EOS
153
- Welcome to RSokoban !
154
-
155
- Goal of Sokoban game is to place each crate on a storage location.
156
- Move the man using the arrow keys.
157
- For a more comprehensive help, please visit the wiki at https://github.com/lkdjiin/RSokoban/wiki.
158
- EOS
159
-
160
- text.insert 'end', help
161
-
162
- @ok = TkButton.new(self) do
163
- text 'OK'
164
- grid('row'=>1, 'column'=>0)
165
- end
166
- @ok.command { ok_on_clic }
167
- end
168
-
169
- def ok_on_clic
170
- destroy
171
- end
172
- end
173
2
 
174
3
  # I am a GUI using tk library.
175
4
  # @note I need the tk-img extension library.
176
5
  # @since 0.73
177
- # @todo a lot of refactoring is needed. There is a lot of duplicated code with Game,
178
- # and maybe Level and BaseUI.
179
- # @todo need some more documentation
6
+ # @todo need some examples and more documentation for private methods
180
7
  class TkUI
181
-
182
- def initialize
183
- @level_loader = RSokoban::LevelLoader.new "microban.xsb"
184
- @level_number = 1
185
- @level = @level_loader.level(@level_number)
186
- @move = 0
8
+ include RSokoban
9
+
10
+ # Build and initialize a GUI with the Tk tool kit.
11
+ # @param [Game] game Where we get the logic.
12
+ def initialize game
13
+ @game = game
187
14
  @last_move = :up
188
15
  @tk_map = []
189
16
  init_gui
190
- display
17
+ start_level
18
+ end
19
+
20
+ # Start the event loop.
21
+ # @since 0.74
22
+ def run
191
23
  Tk.mainloop
192
24
  end
193
25
 
194
26
  private
195
27
 
196
28
  def next_level
197
- @level_number += 1
198
- start_level
29
+ begin
30
+ @game.next_level
31
+ init_level
32
+ rescue LevelNumberTooHighError
33
+ Tk::messageBox :message => "Sorry, no level ##{@game.level_number} in this set."
34
+ end
199
35
  end
200
36
 
201
37
  def start_level
202
38
  begin
203
- @level = @level_loader.level(@level_number)
204
- @move = 0
205
- reset_labels
206
- reset_map
207
- display
208
- rescue RSokoban::LevelNumberTooHighError
209
- Tk::messageBox :message => "Sorry, no level ##{@level_number} in this set."
39
+ @game.start_level
40
+ init_level
41
+ rescue LevelNumberTooHighError
42
+ Tk::messageBox :message => "Sorry, no level ##{@game.level_number} in this set."
210
43
  end
211
44
  end
212
45
 
213
46
  def load_level
214
- d = LevelDialog.new(@tk_root, "Load a level")
215
- if d.ok?
216
- @level_number = d.value
217
- start_level
47
+ d = TkLevelDialog.new(@tk_root, "Load a level")
48
+ return unless d.ok?
49
+ begin
50
+ @game.load_level d.value
51
+ init_level
52
+ rescue LevelNumberTooHighError
53
+ Tk::messageBox :message => "Sorry, no level ##{@game.level_number} in this set."
218
54
  end
219
55
  end
220
56
 
221
57
  def load_set
222
58
  d = SetDialog.new(@tk_root, "Load a set")
223
- new_set = d.value
224
- if d.ok? and new_set != nil
225
- @level_loader = RSokoban::LevelLoader.new new_set
226
- @level_number = 1
227
- start_level
59
+ return unless d.ok?
60
+ return if d.value.nil?
61
+ @game.load_a_new_set d.value
62
+ init_level
63
+ end
64
+
65
+ # For now, map size is restricted to 19x16 on screen.
66
+ def init_level
67
+ if @game.level_width > 19 or @game.level_height > 16
68
+ Tk::messageBox :message => "Sorry, level '#{@game.level_title}' is too big to be displayed."
69
+ @game.restart_set
70
+ end
71
+ reset_labels
72
+ reset_map
73
+ display_initial
74
+ end
75
+
76
+ # Update map rendering. We need only to update man's location and north, south, west
77
+ # and east of him. And because there walls all around the map, there is no needs to check
78
+ # for limits.
79
+ def display_update
80
+ x = @game.man_x
81
+ y = @game.man_y
82
+ update_array = [[x,y], [x+1,y], [x-1,y], [x,y+1], [x,y-1]]
83
+ update_array.each do |x, y|
84
+ case @game.map[y][x].chr
85
+ when WALL then @wall.display_at x, y
86
+ when FLOOR then @floor.display_at x, y
87
+ when CRATE then @crate.display_at x, y
88
+ when STORAGE then @store.display_at x, y
89
+ when MAN then display_man_at x, y
90
+ when MAN_ON_STORAGE then display_man_on_storage_at x, y
91
+ when CRATE_ON_STORAGE then @crate_store.display_at x, y
92
+ end
93
+ end
94
+ end
95
+
96
+ def display_update_after_undo
97
+ x = @game.man_x
98
+ y = @game.man_y
99
+ update_array = [[x,y], [x+1,y], [x+2,y], [x-1,y], [x-2,y], [x,y+1], [x,y+2], [x,y-1], [x,y-2]]
100
+ update_array.each do |x, y|
101
+ next if x < 0 or y < 0
102
+ next if @game.map[y].nil? or @game.map[y][x].nil?
103
+ display_cell_taking_care_of_outside @game.map[y][x].chr, x, y
228
104
  end
229
105
  end
230
106
 
231
- # Display the current map (current state of the level) on screen.
232
- def display
107
+ # Display the initial map on screen.
108
+ def display_initial
233
109
  y = 0
234
- @level.map.each do |row|
110
+ @game.map.each do |row|
235
111
  # find first wall
236
112
  x = row.index(RSokoban::WALL)
237
113
  line = row.strip
238
114
  line.each_char do |char|
239
- case char
240
- when RSokoban::WALL then @wall.display_at x, y
241
- when RSokoban::FLOOR then @floor.display_at x, y
242
- when RSokoban::CRATE then @crate.display_at x, y
243
- when RSokoban::STORAGE then @store.display_at x, y
244
- when RSokoban::MAN then display_man_at x, y
245
- when RSokoban::MAN_ON_STORAGE then display_man_on_storage_at x, y
246
- when RSokoban::CRATE_ON_STORAGE then @crate_store.display_at x, y
247
- end
115
+ display_cell_taking_care_of_outside char, x, y
248
116
  x += 1
249
117
  end
250
118
  y += 1
251
119
  end
252
120
  end
253
121
 
122
+ def display_cell_taking_care_of_outside char, x, y
123
+ case char
124
+ when WALL then @wall.display_at x, y
125
+ when FLOOR then display_floor_at x, y
126
+ when CRATE then @crate.display_at x, y
127
+ when STORAGE then @store.display_at x, y
128
+ when MAN then display_man_at x, y
129
+ when MAN_ON_STORAGE then display_man_on_storage_at x, y
130
+ when CRATE_ON_STORAGE then @crate_store.display_at x, y
131
+ end
132
+ end
133
+
134
+ def display_floor_at x, y
135
+ return if y == 0
136
+ height = y - 1
137
+ height.downto(0).each {|row|
138
+ break if @game.map[row][x].nil?
139
+ if [WALL, FLOOR, CRATE, STORAGE].include?(@game.map[row][x].chr)
140
+ @floor.display_at x, y
141
+ break
142
+ end
143
+ }
144
+ end
145
+
254
146
  def display_man_at x, y
255
147
  case @last_move
256
148
  when :up then @man_up.display_at x, y
@@ -302,8 +194,10 @@ EOS
302
194
  file.add :command, :label => 'Load level', :command => proc{load_level}, :accelerator => 'Ctrl+L'
303
195
  file.add :command, :label => 'Load set', :command => proc{load_set}
304
196
  file.add :separator
305
- file.add :command, :label => 'Undo', :command => proc{undo}, :accelerator => 'Ctrl+U'
197
+ file.add :command, :label => 'Undo', :command => proc{undo}, :accelerator => 'Ctrl+Z'
198
+ file.add :command, :label => 'Redo', :command => proc{my_redo}, :accelerator => 'Ctrl+Y'
306
199
  file.add :command, :label => 'Restart level', :command => proc{start_level}, :accelerator => 'Ctrl+R'
200
+ file.add :command, :label => 'Next level', :command => proc{next_level}, :accelerator => 'Ctrl+N'
307
201
  file.add :separator
308
202
  file.add :command, :label => 'Quit', :command => proc{exit}
309
203
 
@@ -313,33 +207,33 @@ EOS
313
207
  end
314
208
 
315
209
  def init_labels
316
- @tk_label_set = TkLabel.new(@tk_root) do
317
- grid('row' => 0, 'column' => 0, 'columnspan' => 19, 'sticky' => 'w')
210
+ @tk_frame_label = TkFrame.new(@tk_root) do
211
+ grid('row' => 0, 'column' => 0, 'columnspan' => 19, 'sticky' => 'w')
212
+ padx 5
213
+ pady 5
318
214
  end
319
- @tk_label_level = TkLabel.new(@tk_root) do
320
- grid('row'=>1, 'column'=>0, 'columnspan' => 19, 'sticky' => 'w')
215
+ @tk_label_set = TkLabel.new(@tk_frame_label) do
216
+ grid('row' => 0, 'column' => 0, 'sticky' => 'w')
321
217
  end
322
- @tk_label_move = TkLabel.new(@tk_root) do
323
- grid('row'=>2, 'column'=>0, 'columnspan' => 19, 'sticky' => 'w')
218
+ @tk_label_level = TkLabel.new(@tk_frame_label) do
219
+ grid('row'=>1, 'column'=> 0, 'sticky' => 'w')
324
220
  end
325
- @tk_label_separator = TkLabel.new(@tk_root) do
326
- text("")
327
- grid('row'=>4, 'column'=>0, 'columnspan' => 19)
221
+ @tk_label_move = TkLabel.new(@tk_frame_label) do
222
+ grid('row'=>2, 'column'=>0, 'sticky' => 'w')
328
223
  end
329
- reset_labels
330
224
  end
331
225
 
332
226
  def reset_labels
333
- @tk_label_set.configure('text' => "Set: #{@level_loader.set.title}")
334
- @tk_label_level.configure('text' => "Level: #{@level.title} (#{@level_number}/#{@level_loader.set.size})")
227
+ @tk_label_set.configure('text' => "Set: #{@game.set_title}")
228
+ @tk_label_level.configure('text' => "Level: #{@game.level_title} (#{@game.level_number}/#{@game.set_size})")
335
229
  update_move_information
336
230
  end
337
231
 
338
232
  def update_move_information
339
- @tk_label_move.configure('text' => "Move: #{@move}")
233
+ @tk_label_move.configure('text' => "Move: #{@game.move_number}")
340
234
  end
341
235
 
342
- # Build the map of labels. Images of the game will be displayed on those labels.
236
+ # Build the map of labels. TkBoxs of the game will be displayed on those labels.
343
237
  def init_map
344
238
  row_in_grid = 5
345
239
  (0...16).each {|row_index|
@@ -367,24 +261,33 @@ EOS
367
261
  end
368
262
 
369
263
  def init_buttons
370
- @tk_undo_button = TkButton.new(@tk_root) do
264
+ @tk_frame_button = TkFrame.new(@tk_root) do
265
+ grid('row' => 1, 'column' => 0, 'columnspan' => 19, 'sticky' => 'w')
266
+ padx 5
267
+ pady 5
268
+ end
269
+ @tk_undo_button = TkButton.new(@tk_frame_button) do
371
270
  text 'Undo'
372
- grid('row'=>3, 'column'=>0, 'columnspan' => 3)
271
+ grid('row'=> 0, 'column'=> 0)
272
+ end
273
+ @tk_redo_button = TkButton.new(@tk_frame_button) do
274
+ text 'Redo'
275
+ grid('row'=> 0, 'column'=> 1)
373
276
  end
374
277
 
375
- @tk_retry_button = TkButton.new(@tk_root) do
278
+ @tk_retry_button = TkButton.new(@tk_frame_button) do
376
279
  text 'Retry'
377
- grid('row'=>3, 'column'=>3, 'columnspan' => 3)
280
+ grid('row'=> 0, 'column'=> 2)
378
281
  end
379
282
 
380
- @tk_level_button = TkButton.new(@tk_root) do
283
+ @tk_level_button = TkButton.new(@tk_frame_button) do
381
284
  text 'Level'
382
- grid('row'=>3, 'column'=>6, 'columnspan' => 3)
285
+ grid('row'=> 0, 'column'=> 3)
383
286
  end
384
287
 
385
- @tk_set_button = TkButton.new(@tk_root) do
386
- text 'Set'
387
- grid('row'=>3, 'column'=>9, 'columnspan' => 3)
288
+ @tk_next_level_button = TkButton.new(@tk_frame_button) do
289
+ text 'Next'
290
+ grid('row'=> 0, 'column'=> 4)
388
291
  end
389
292
  end
390
293
 
@@ -394,36 +297,41 @@ EOS
394
297
  @tk_root.bind('Down') { move :down }
395
298
  @tk_root.bind('Left') { move :left }
396
299
  @tk_root.bind('Right') { move :right }
397
- @tk_root.bind('Control-u') { undo }
300
+ @tk_root.bind('Control-z') { undo }
301
+ @tk_root.bind('Control-y') { my_redo }
398
302
  @tk_root.bind('Control-r') { start_level }
399
303
  @tk_root.bind('Control-l') { load_level }
304
+ @tk_root.bind('Control-n') { next_level }
400
305
  @tk_root.bind('F1') { help }
401
306
  @tk_undo_button.command { undo }
307
+ @tk_redo_button.command { my_redo }
402
308
  @tk_retry_button.command { start_level }
403
309
  @tk_level_button.command { load_level }
404
- @tk_set_button.command { load_set }
310
+ @tk_next_level_button.command { next_level }
405
311
  end
406
312
 
407
313
  def undo
408
- result = @level.undo
409
- @move = get_move_number_from_result_of_last_move result
314
+ result = @game.undo
410
315
  update_move_information
411
- display
316
+ display_update_after_undo
317
+ end
318
+
319
+ def my_redo
320
+ result = @game.redo
321
+ update_move_information
322
+ display_update_after_undo
412
323
  end
413
324
 
414
325
  # Send the move to Level and process response.
415
- # @todo the last move is recorded here to permit the display of the man
416
- # in the 4 direction. This is a bad thing ! Level#move should returns a hash
417
- # with all needed information (status, move number, last move, error message, etc.)
326
+ # @param [:ip, :down, :left, :right]
418
327
  def move symb
419
328
  @last_move = symb
420
- result = @level.move symb
421
- unless result.start_with?('ERROR')
422
- @move = get_move_number_from_result_of_last_move result
329
+ result = @game.move symb
330
+ unless result.error?
423
331
  update_move_information
424
- display
332
+ display_update
425
333
  end
426
- if result.start_with?('WIN')
334
+ if result.win?
427
335
  response = Tk::messageBox :type => 'yesno', :message => "Level completed !\nPlay next level ?",
428
336
  :icon => 'question', :title => 'You win !', :parent => @tk_root, :default => 'yes'
429
337
  next_level if response == 'yes'
@@ -431,29 +339,22 @@ EOS
431
339
  end
432
340
  end
433
341
 
434
- # Assuming that result start with 'OK' or 'WIN'
435
- # @return [Fixnum] current move number
436
- def get_move_number_from_result_of_last_move result
437
- move_index = result =~ /\d+/
438
- result[move_index..-1].to_i
439
- end
440
-
441
342
  def preload_images
442
343
  dir = $RSOKOBAN_PATH + '/skins/default/'
443
- @wall = Image.new(dir + 'wall.bmp', @tk_map)
444
- @crate = Image.new(dir + 'crate.bmp', @tk_map)
445
- @floor = Image.new(dir + 'floor.bmp', @tk_map)
446
- @store = Image.new(dir + 'store.bmp', @tk_map)
447
- @man_up = Image.new(dir + 'man_up.bmp', @tk_map)
448
- @man_down = Image.new(dir + 'man_down.bmp', @tk_map)
449
- @man_left = Image.new(dir + 'man_left.bmp', @tk_map)
450
- @man_right = Image.new(dir + 'man_right.bmp', @tk_map)
451
- @crate_store = Image.new(dir + 'crate_store.bmp', @tk_map)
452
- @man_store_up = Image.new(dir + 'man_store_up.bmp', @tk_map)
453
- @man_store_down = Image.new(dir + 'man_store_down.bmp', @tk_map)
454
- @man_store_left = Image.new(dir + 'man_store_left.bmp', @tk_map)
455
- @man_store_right = Image.new(dir + 'man_store_right.bmp', @tk_map)
456
- @outside = Image.new(dir + 'outside.bmp', @tk_map)
344
+ @wall = TkBox.new(dir + 'wall.bmp', @tk_map)
345
+ @crate = TkBox.new(dir + 'crate.bmp', @tk_map)
346
+ @floor = TkBox.new(dir + 'floor.bmp', @tk_map)
347
+ @store = TkBox.new(dir + 'store.bmp', @tk_map)
348
+ @man_up = TkBox.new(dir + 'man_up.bmp', @tk_map)
349
+ @man_down = TkBox.new(dir + 'man_down.bmp', @tk_map)
350
+ @man_left = TkBox.new(dir + 'man_left.bmp', @tk_map)
351
+ @man_right = TkBox.new(dir + 'man_right.bmp', @tk_map)
352
+ @crate_store = TkBox.new(dir + 'crate_store.bmp', @tk_map)
353
+ @man_store_up = TkBox.new(dir + 'man_store_up.bmp', @tk_map)
354
+ @man_store_down = TkBox.new(dir + 'man_store_down.bmp', @tk_map)
355
+ @man_store_left = TkBox.new(dir + 'man_store_left.bmp', @tk_map)
356
+ @man_store_right = TkBox.new(dir + 'man_store_right.bmp', @tk_map)
357
+ @outside = TkBox.new(dir + 'outside.bmp', @tk_map)
457
358
  end
458
359
 
459
360
  def help