RSokoban 0.73 → 0.74

Sign up to get free protection for your applications and to get access to all the features.
@@ -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