RSokoban 0.71 → 0.73

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.
Files changed (50) hide show
  1. data/NEWS +22 -0
  2. data/README.rdoc +19 -19
  3. data/RSokoban-0.73.gem +0 -0
  4. data/TODO +25 -18
  5. data/VERSION +1 -0
  6. data/bin/rsokoban +65 -1
  7. data/lib/rsokoban/exception.rb +4 -0
  8. data/lib/rsokoban/game.rb +42 -29
  9. data/lib/rsokoban/level.rb +67 -30
  10. data/lib/rsokoban/level_loader.rb +4 -4
  11. data/lib/rsokoban/level_set.rb +20 -10
  12. data/lib/rsokoban/map.rb +51 -0
  13. data/lib/rsokoban/move_recorder.rb +38 -0
  14. data/lib/rsokoban/option.rb +29 -12
  15. data/lib/rsokoban/raw_level.rb +15 -4
  16. data/lib/rsokoban/ui/base_ui.rb +37 -0
  17. data/lib/rsokoban/ui/console.rb +43 -33
  18. data/lib/rsokoban/ui/curses_console.rb +45 -36
  19. data/lib/rsokoban/ui/player_action.rb +74 -0
  20. data/lib/rsokoban/ui/tk_ui.rb +474 -0
  21. data/lib/rsokoban/ui.rb +10 -0
  22. data/lib/rsokoban.rb +4 -5
  23. data/skins/default/crate.bmp +0 -0
  24. data/skins/default/crate_store.bmp +0 -0
  25. data/skins/default/floor.bmp +0 -0
  26. data/skins/default/man_down.bmp +0 -0
  27. data/skins/default/man_left.bmp +0 -0
  28. data/skins/default/man_right.bmp +0 -0
  29. data/skins/default/man_store_down.bmp +0 -0
  30. data/skins/default/man_store_left.bmp +0 -0
  31. data/skins/default/man_store_right.bmp +0 -0
  32. data/skins/default/man_store_up.bmp +0 -0
  33. data/skins/default/man_up.bmp +0 -0
  34. data/skins/default/outside.bmp +0 -0
  35. data/skins/default/readme +1 -0
  36. data/skins/default/store.bmp +0 -0
  37. data/skins/default/wall.bmp +0 -0
  38. data/test/original.xsb +0 -2
  39. data/test/tc_level.rb +37 -37
  40. data/test/tc_level_loader.rb +14 -2
  41. data/test/tc_level_set.rb +8 -0
  42. data/test/tc_map.rb +40 -0
  43. data/test/tc_move_recorder.rb +100 -0
  44. data/test/tc_raw_level.rb +5 -5
  45. data/test/test.rb +3 -1
  46. data/test/test_file2.xsb +2 -0
  47. data/test/ui/tc_console.rb +27 -13
  48. data/test/ui/tc_player_action.rb +156 -0
  49. metadata +38 -25
  50. data/lib/rsokoban/ui/ui.rb +0 -44
@@ -0,0 +1,474 @@
1
+ #require 'tk'
2
+ #require 'tkextlib/tkimg'
3
+
4
+ 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
+
174
+ # I am a GUI using tk library.
175
+ # @note I need the tk-img extension library.
176
+ # @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
180
+ 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
187
+ @last_move = :up
188
+ @tk_map = []
189
+ init_gui
190
+ display
191
+ Tk.mainloop
192
+ end
193
+
194
+ private
195
+
196
+ def next_level
197
+ @level_number += 1
198
+ start_level
199
+ end
200
+
201
+ def start_level
202
+ 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."
210
+ end
211
+ end
212
+
213
+ def load_level
214
+ d = LevelDialog.new(@tk_root, "Load a level")
215
+ if d.ok?
216
+ @level_number = d.value
217
+ start_level
218
+ end
219
+ end
220
+
221
+ def load_set
222
+ 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
228
+ end
229
+ end
230
+
231
+ # Display the current map (current state of the level) on screen.
232
+ def display
233
+ y = 0
234
+ @level.map.each do |row|
235
+ # find first wall
236
+ x = row.index(RSokoban::WALL)
237
+ line = row.strip
238
+ 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
248
+ x += 1
249
+ end
250
+ y += 1
251
+ end
252
+ end
253
+
254
+ def display_man_at x, y
255
+ case @last_move
256
+ when :up then @man_up.display_at x, y
257
+ when :down then @man_down.display_at x, y
258
+ when :left then @man_left.display_at x, y
259
+ else
260
+ @man_right.display_at x, y
261
+ end
262
+ end
263
+
264
+ def display_man_on_storage_at x, y
265
+ case @last_move
266
+ when :up then @man_store_up.display_at x, y
267
+ when :down then @man_store_down.display_at x, y
268
+ when :left then @man_store_left.display_at x, y
269
+ else
270
+ @man_store_right.display_at x, y
271
+ end
272
+ end
273
+
274
+ def init_gui
275
+ init_root
276
+ init_menu
277
+ init_labels
278
+ preload_images
279
+ init_map
280
+ init_buttons
281
+ make_binding
282
+ end
283
+
284
+ def init_root
285
+ @tk_root = TkRoot.new do
286
+ title "RSokoban " + File.read($RSOKOBAN_PATH + '/VERSION').strip
287
+ minsize(400, 400)
288
+ resizable(false, false)
289
+ end
290
+ end
291
+
292
+ def init_menu
293
+ TkOption.add '*tearOff', 0
294
+ menubar = TkMenu.new(@tk_root)
295
+ @tk_root['menu'] = menubar
296
+
297
+ file = TkMenu.new(menubar)
298
+ helpm = TkMenu.new(menubar)
299
+ menubar.add :cascade, :menu => file, :label => 'File'
300
+ menubar.add :cascade, :menu => helpm, :label => 'Help'
301
+
302
+ file.add :command, :label => 'Load level', :command => proc{load_level}, :accelerator => 'Ctrl+L'
303
+ file.add :command, :label => 'Load set', :command => proc{load_set}
304
+ file.add :separator
305
+ file.add :command, :label => 'Undo', :command => proc{undo}, :accelerator => 'Ctrl+U'
306
+ file.add :command, :label => 'Restart level', :command => proc{start_level}, :accelerator => 'Ctrl+R'
307
+ file.add :separator
308
+ file.add :command, :label => 'Quit', :command => proc{exit}
309
+
310
+ helpm.add :command, :label => 'Help', :command => proc{help}, :accelerator => 'F1'
311
+ helpm.add :separator
312
+ helpm.add :command, :label => 'About', :command => proc{about}
313
+ end
314
+
315
+ def init_labels
316
+ @tk_label_set = TkLabel.new(@tk_root) do
317
+ grid('row' => 0, 'column' => 0, 'columnspan' => 19, 'sticky' => 'w')
318
+ end
319
+ @tk_label_level = TkLabel.new(@tk_root) do
320
+ grid('row'=>1, 'column'=>0, 'columnspan' => 19, 'sticky' => 'w')
321
+ end
322
+ @tk_label_move = TkLabel.new(@tk_root) do
323
+ grid('row'=>2, 'column'=>0, 'columnspan' => 19, 'sticky' => 'w')
324
+ end
325
+ @tk_label_separator = TkLabel.new(@tk_root) do
326
+ text("")
327
+ grid('row'=>4, 'column'=>0, 'columnspan' => 19)
328
+ end
329
+ reset_labels
330
+ end
331
+
332
+ 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})")
335
+ update_move_information
336
+ end
337
+
338
+ def update_move_information
339
+ @tk_label_move.configure('text' => "Move: #{@move}")
340
+ end
341
+
342
+ # Build the map of labels. Images of the game will be displayed on those labels.
343
+ def init_map
344
+ row_in_grid = 5
345
+ (0...16).each {|row_index|
346
+ row = []
347
+ (0...19).each {|col_index|
348
+ label = TkLabel.new(@tk_root) do
349
+ grid('row'=> row_in_grid, 'column'=> col_index, 'padx' => 0, 'pady' => 0, 'ipadx' => 0, 'ipady' => 0)
350
+ end
351
+ label['borderwidth'] = 0
352
+ row.push label
353
+ }
354
+ @tk_map.push row
355
+ row_in_grid += 1
356
+ }
357
+ reset_map
358
+ end
359
+
360
+ # Reset all the map with 'outside' tile.
361
+ # @todo little improvement : reload @outside image only if there is something else
362
+ # in the current map.
363
+ def reset_map
364
+ @tk_map.each_index {|y|
365
+ @tk_map[y].each_index {|x| @outside.display_at(x, y) }
366
+ }
367
+ end
368
+
369
+ def init_buttons
370
+ @tk_undo_button = TkButton.new(@tk_root) do
371
+ text 'Undo'
372
+ grid('row'=>3, 'column'=>0, 'columnspan' => 3)
373
+ end
374
+
375
+ @tk_retry_button = TkButton.new(@tk_root) do
376
+ text 'Retry'
377
+ grid('row'=>3, 'column'=>3, 'columnspan' => 3)
378
+ end
379
+
380
+ @tk_level_button = TkButton.new(@tk_root) do
381
+ text 'Level'
382
+ grid('row'=>3, 'column'=>6, 'columnspan' => 3)
383
+ end
384
+
385
+ @tk_set_button = TkButton.new(@tk_root) do
386
+ text 'Set'
387
+ grid('row'=>3, 'column'=>9, 'columnspan' => 3)
388
+ end
389
+ end
390
+
391
+ # Bind user's actions
392
+ def make_binding
393
+ @tk_root.bind('Up') { move :up }
394
+ @tk_root.bind('Down') { move :down }
395
+ @tk_root.bind('Left') { move :left }
396
+ @tk_root.bind('Right') { move :right }
397
+ @tk_root.bind('Control-u') { undo }
398
+ @tk_root.bind('Control-r') { start_level }
399
+ @tk_root.bind('Control-l') { load_level }
400
+ @tk_root.bind('F1') { help }
401
+ @tk_undo_button.command { undo }
402
+ @tk_retry_button.command { start_level }
403
+ @tk_level_button.command { load_level }
404
+ @tk_set_button.command { load_set }
405
+ end
406
+
407
+ def undo
408
+ result = @level.undo
409
+ @move = get_move_number_from_result_of_last_move result
410
+ update_move_information
411
+ display
412
+ end
413
+
414
+ # 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.)
418
+ def move symb
419
+ @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
423
+ update_move_information
424
+ display
425
+ end
426
+ if result.start_with?('WIN')
427
+ response = Tk::messageBox :type => 'yesno', :message => "Level completed !\nPlay next level ?",
428
+ :icon => 'question', :title => 'You win !', :parent => @tk_root, :default => 'yes'
429
+ next_level if response == 'yes'
430
+ start_level if response == 'no'
431
+ end
432
+ end
433
+
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
+ def preload_images
442
+ 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)
457
+ end
458
+
459
+ def help
460
+ HelpDialog.new(@tk_root, "RSokoban Help")
461
+ end
462
+
463
+ def about
464
+ text = "RSokoban #{File.read($RSOKOBAN_PATH + '/VERSION').strip} \n"
465
+ text += "This is free software !\n"
466
+ text += "Copyright 2011, Xavier Nayrac\n"
467
+ text += "Licensed under the GPL-3\n"
468
+ text += "Contact: xavier.nayrac@gmail.com"
469
+ Tk::messageBox :message => text, :title => 'About'
470
+ end
471
+
472
+ end
473
+
474
+ end
@@ -0,0 +1,10 @@
1
+ require "rsokoban/ui/base_ui"
2
+ require "rsokoban/ui/player_action"
3
+
4
+ module RSokoban
5
+
6
+ # Module dedicated to user interfaces.
7
+ module UI
8
+ end
9
+
10
+ end
data/lib/rsokoban.rb CHANGED
@@ -9,13 +9,12 @@ require "rsokoban/game"
9
9
  require "rsokoban/option"
10
10
  require "rsokoban/level_set"
11
11
  require "rsokoban/raw_level"
12
- require "rsokoban/ui/ui"
12
+ require "rsokoban/ui"
13
+ require "rsokoban/move_recorder"
14
+ require "rsokoban/map"
13
15
 
14
16
  # I am the main module of the game.
15
- module RSokoban
16
- # Version of the program
17
- VERSION = '0.71'
18
-
17
+ module RSokoban
19
18
  # Game elements.
20
19
  # Those constants are used intensively.
21
20
  MAN = '@'
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ Blue Granite by MerlijnVV
Binary file
Binary file
data/test/original.xsb CHANGED
@@ -1493,5 +1493,3 @@
1493
1493
  ####################
1494
1494
  ; 90
1495
1495
  ; Copyright: Thinking Rabbit
1496
-
1497
-