RSokoban 0.74 → 0.76

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 (111) hide show
  1. data/NEWS +33 -1
  2. data/README.rdoc +21 -14
  3. data/TODO +6 -12
  4. data/VERSION +1 -1
  5. data/bin/rsokoban +40 -3
  6. data/data/big.xsb +35 -0
  7. data/lib/rsokoban/config.rb +34 -0
  8. data/lib/rsokoban/crate.rb +4 -5
  9. data/lib/rsokoban/exception.rb +2 -0
  10. data/lib/rsokoban/game/game.rb +153 -0
  11. data/lib/rsokoban/game/game_curses.rb +11 -0
  12. data/lib/rsokoban/game/game_factory.rb +23 -0
  13. data/lib/rsokoban/game/game_gui.rb +27 -0
  14. data/lib/rsokoban/game/game_portable.rb +10 -0
  15. data/lib/rsokoban/game/game_tk.rb +11 -0
  16. data/lib/rsokoban/game/game_ui.rb +81 -0
  17. data/lib/rsokoban/game.rb +7 -238
  18. data/lib/rsokoban/install.rb +22 -0
  19. data/lib/rsokoban/layered_map.rb +139 -0
  20. data/lib/rsokoban/level.rb +112 -166
  21. data/lib/rsokoban/level_set.rb +4 -4
  22. data/lib/rsokoban/man.rb +3 -4
  23. data/lib/rsokoban/map.rb +91 -15
  24. data/lib/rsokoban/move_result.rb +2 -2
  25. data/lib/rsokoban/option.rb +4 -4
  26. data/lib/rsokoban/position.rb +1 -1
  27. data/lib/rsokoban/raw_level.rb +11 -2
  28. data/lib/rsokoban/record.rb +51 -0
  29. data/lib/rsokoban/set_loader.rb +108 -0
  30. data/lib/rsokoban/ui/base_ui.rb +1 -0
  31. data/lib/rsokoban/ui/console.rb +56 -25
  32. data/lib/rsokoban/ui/curses_console.rb +59 -33
  33. data/lib/rsokoban/ui/player_action.rb +13 -7
  34. data/lib/rsokoban/ui/skin.rb +105 -0
  35. data/lib/rsokoban/ui/tk_dialogs.rb +104 -18
  36. data/lib/rsokoban/ui/tk_ui.rb +410 -233
  37. data/lib/rsokoban/ui.rb +1 -0
  38. data/lib/rsokoban.rb +14 -2
  39. data/skins/AntiqueDesk/crate.bmp +0 -0
  40. data/skins/AntiqueDesk/crate_store.bmp +0 -0
  41. data/skins/AntiqueDesk/floor.bmp +0 -0
  42. data/skins/AntiqueDesk/man_down.bmp +0 -0
  43. data/skins/AntiqueDesk/man_left.bmp +0 -0
  44. data/skins/AntiqueDesk/man_right.bmp +0 -0
  45. data/skins/AntiqueDesk/man_store_down.bmp +0 -0
  46. data/skins/AntiqueDesk/man_store_left.bmp +0 -0
  47. data/skins/AntiqueDesk/man_store_right.bmp +0 -0
  48. data/skins/AntiqueDesk/man_store_up.bmp +0 -0
  49. data/skins/AntiqueDesk/man_up.bmp +0 -0
  50. data/skins/AntiqueDesk/outside.bmp +0 -0
  51. data/skins/AntiqueDesk/skin.conf +3 -0
  52. data/skins/AntiqueDesk/store.bmp +0 -0
  53. data/skins/AntiqueDesk/wall.bmp +0 -0
  54. data/skins/BlueGranite/outside.bmp +0 -0
  55. data/skins/BlueGranite/skin.conf +3 -0
  56. data/skins/HeavyMetal/crate.bmp +0 -0
  57. data/skins/HeavyMetal/crate_store.bmp +0 -0
  58. data/skins/HeavyMetal/floor.bmp +0 -0
  59. data/skins/HeavyMetal/man.bmp +0 -0
  60. data/skins/HeavyMetal/man_store.bmp +0 -0
  61. data/skins/HeavyMetal/outside.bmp +0 -0
  62. data/skins/HeavyMetal/skin.conf +3 -0
  63. data/skins/HeavyMetal/store.bmp +0 -0
  64. data/skins/HeavyMetal/wall.bmp +0 -0
  65. data/skins/HeavyMetal/wall_d.bmp +0 -0
  66. data/skins/HeavyMetal/wall_dl.bmp +0 -0
  67. data/skins/HeavyMetal/wall_dlr.bmp +0 -0
  68. data/skins/HeavyMetal/wall_dr.bmp +0 -0
  69. data/skins/HeavyMetal/wall_l.bmp +0 -0
  70. data/skins/HeavyMetal/wall_lr.bmp +0 -0
  71. data/skins/HeavyMetal/wall_r.bmp +0 -0
  72. data/skins/HeavyMetal/wall_u.bmp +0 -0
  73. data/skins/HeavyMetal/wall_ud.bmp +0 -0
  74. data/skins/HeavyMetal/wall_udl.bmp +0 -0
  75. data/skins/HeavyMetal/wall_udlr.bmp +0 -0
  76. data/skins/HeavyMetal/wall_udr.bmp +0 -0
  77. data/skins/HeavyMetal/wall_ul.bmp +0 -0
  78. data/skins/HeavyMetal/wall_ulr.bmp +0 -0
  79. data/skins/HeavyMetal/wall_ur.bmp +0 -0
  80. data/test/record/original.yaml +2 -0
  81. data/test/tc_game.rb +24 -15
  82. data/test/tc_game_factory.rb +40 -0
  83. data/test/tc_game_gui.rb +26 -0
  84. data/test/tc_game_ui.rb +153 -0
  85. data/test/tc_install.rb +12 -0
  86. data/test/tc_layered_map.rb +105 -0
  87. data/test/tc_level.rb +109 -107
  88. data/test/tc_level_set.rb +4 -4
  89. data/test/tc_map.rb +46 -10
  90. data/test/tc_record.rb +100 -0
  91. data/test/{tc_level_loader.rb → tc_set_loader.rb} +25 -24
  92. data/test/test.rb +9 -2
  93. data/test/ui/tc_skin.rb +71 -0
  94. metadata +89 -26
  95. data/lib/rsokoban/level_loader.rb +0 -81
  96. data/lib/rsokoban/ui/tk_box.rb +0 -21
  97. data/skins/default/outside.bmp +0 -0
  98. data/skins/default/readme +0 -1
  99. /data/skins/{default → BlueGranite}/crate.bmp +0 -0
  100. /data/skins/{default → BlueGranite}/crate_store.bmp +0 -0
  101. /data/skins/{default → BlueGranite}/floor.bmp +0 -0
  102. /data/skins/{default → BlueGranite}/man_down.bmp +0 -0
  103. /data/skins/{default → BlueGranite}/man_left.bmp +0 -0
  104. /data/skins/{default → BlueGranite}/man_right.bmp +0 -0
  105. /data/skins/{default → BlueGranite}/man_store_down.bmp +0 -0
  106. /data/skins/{default → BlueGranite}/man_store_left.bmp +0 -0
  107. /data/skins/{default → BlueGranite}/man_store_right.bmp +0 -0
  108. /data/skins/{default → BlueGranite}/man_store_up.bmp +0 -0
  109. /data/skins/{default → BlueGranite}/man_up.bmp +0 -0
  110. /data/skins/{default → BlueGranite}/store.bmp +0 -0
  111. /data/skins/{default → BlueGranite}/wall.bmp +0 -0
@@ -1,19 +1,45 @@
1
1
  module RSokoban::UI
2
2
 
3
3
  # I am a GUI using tk library.
4
+ #
5
+ # The grid is:
6
+ # * a frame with some labels (game info)
7
+ # * a frame with some buttons (menu shortcuts)
8
+ # * a frame with the rendered map
9
+ #
4
10
  # @note I need the tk-img extension library.
11
+ # @note This code is untestable and untested. In fact, I don't know HOW to test it !
5
12
  # @since 0.73
6
- # @todo need some examples and more documentation for private methods
7
13
  class TkUI
8
14
  include RSokoban
9
15
 
16
+ # Number of maximum displayed cells in a row
17
+ MAP_WIDTH = 19
18
+ # Number of maximum displayed cells in a column
19
+ MAP_HEIGHT = 16
20
+
10
21
  # Build and initialize a GUI with the Tk tool kit.
11
22
  # @param [Game] game Where we get the logic.
12
23
  def initialize game
13
24
  @game = game
14
25
  @last_move = :up
15
- @tk_map = []
16
- init_gui
26
+ @images = {}
27
+ @x_bounds = []
28
+ @y_bounds = []
29
+ @cell_size = 30
30
+ init_root
31
+ Menu.new(@tk_root, self)
32
+ @tk_frame_label = FrameOfLabels.new @tk_root
33
+
34
+ config = Config.new
35
+ skin_name = config['skin']
36
+ load_skin Skin.new.path_of_skin skin_name
37
+
38
+ #load_skin File.join($RSOKOBAN_PATH, 'skins', 'default')
39
+ init_map
40
+ @tk_frame_button = FrameOfButtons.new @tk_root
41
+ make_binding
42
+
17
43
  start_level
18
44
  end
19
45
 
@@ -23,7 +49,30 @@ module RSokoban::UI
23
49
  Tk.mainloop
24
50
  end
25
51
 
26
- private
52
+ def undo
53
+ result = @game.undo
54
+ @tk_frame_label.update_move_information @game
55
+ display_update_after_undo
56
+ end
57
+
58
+ def my_redo
59
+ result = @game.redo
60
+ @tk_frame_label.update_move_information @game
61
+ display_update_after_undo
62
+ end
63
+
64
+ def help
65
+ HelpDialog.new(@tk_root, "RSokoban Help")
66
+ end
67
+
68
+ def about
69
+ text = "RSokoban #{File.read($RSOKOBAN_PATH + '/VERSION').strip} \n"
70
+ text += "This is free software !\n"
71
+ text += "Copyright 2011, Xavier Nayrac\n"
72
+ text += "Licensed under the GPL-3\n"
73
+ text += "Contact: xavier.nayrac@gmail.com"
74
+ Tk::messageBox :message => text, :title => 'About'
75
+ end
27
76
 
28
77
  def next_level
29
78
  begin
@@ -44,10 +93,10 @@ module RSokoban::UI
44
93
  end
45
94
 
46
95
  def load_level
47
- d = TkLevelDialog.new(@tk_root, "Load a level")
48
- return unless d.ok?
96
+ dial = TkLevelDialog.new(@tk_root, "Load a level")
97
+ return unless dial.ok?
49
98
  begin
50
- @game.load_level d.value
99
+ @game.load_level dial.value
51
100
  init_level
52
101
  rescue LevelNumberTooHighError
53
102
  Tk::messageBox :message => "Sorry, no level ##{@game.level_number} in this set."
@@ -55,244 +104,260 @@ module RSokoban::UI
55
104
  end
56
105
 
57
106
  def load_set
58
- d = SetDialog.new(@tk_root, "Load a set")
59
- return unless d.ok?
60
- return if d.value.nil?
61
- @game.load_a_new_set d.value
107
+ dial = SetDialog.new(@tk_root, "Load a set")
108
+ return unless dial.ok?
109
+ return if dial.value.nil?
110
+ @game.load_a_new_set dial.value
62
111
  init_level
63
112
  end
64
113
 
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
114
+ def scroll_right
115
+ @left_window += 10
116
+ if @left_window + MAP_WIDTH > @game.level_width
117
+ @left_window = @game.level_width - MAP_WIDTH
118
+ end
119
+ @left_window = 0 if @left_window < 0
120
+ window_update
121
+ end
122
+
123
+ def scroll_left
124
+ @left_window -= 10
125
+ @left_window = 0 if @left_window < 0
126
+ window_update
127
+ end
128
+
129
+ def scroll_up
130
+ @top_window -= 10
131
+ @top_window = 0 if @top_window < 0
132
+ window_update
133
+ end
134
+
135
+ def scroll_down
136
+ @top_window += 10
137
+ if @top_window + MAP_HEIGHT > @game.level_height
138
+ @top_window = @game.level_height - MAP_HEIGHT
70
139
  end
71
- reset_labels
140
+ @top_window = 0 if @top_window < 0
141
+ window_update
142
+ end
143
+
144
+ # @since 0.76
145
+ def select_skin
146
+ dial = SkinDialog.new(@tk_root, "Change skin")
147
+ return unless dial.ok?
148
+ return if dial.value.nil?
149
+ load_skin dial.value
150
+ conf = Config.new
151
+ conf['skin'] = File.basename dial.value
152
+ init_level
153
+ end
154
+
155
+ private
156
+
157
+ def init_level
158
+ width, height = get_dimension_in_cells
159
+ compute_bounds width, height
160
+ @top_window = @left_window = 0
161
+ @tk_frame_label.reset_labels @game
162
+ @tk_frame_render.geometry width, height, @cell_size
72
163
  reset_map
73
164
  display_initial
74
165
  end
75
166
 
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
167
+ def get_dimension_in_cells
168
+ width = @game.level_width > MAP_WIDTH ? MAP_WIDTH : @game.level_width
169
+ height = @game.level_height > MAP_HEIGHT ? MAP_HEIGHT : @game.level_height
170
+ [width, height]
171
+ end
172
+
173
+ def compute_bounds width, height
174
+ @x_bounds = []
175
+ (0...width).each {|idx| @x_bounds.push idx * @cell_size}
176
+ @y_bounds = []
177
+ (0...height).each {|idx| @y_bounds.push idx * @cell_size}
178
+ end
179
+
180
+ # @todo no needs to update wall tiles
181
+ def display update_locations
182
+ return if man_goes_offscreen?
183
+ map = window
184
+ update_locations.each do |x, y|
185
+ next if y >= @top_window + MAP_HEIGHT
186
+ next if x >= @left_window + MAP_WIDTH
187
+ next if x < 0 or y < 0
188
+ # I think there should be something wrong with the tests above.
189
+ # Else I don't need the two following tests.
190
+ #row = window[y]
191
+ row = map[y]
192
+ next if row.nil?
193
+ cell = row[x]
194
+ next if cell.nil?
195
+ display_cell_taking_care_of_content cell.chr, x, y
93
196
  end
94
197
  end
95
198
 
199
+ # We need only to update man's location and north, south, west
200
+ # and east of him.
201
+ def display_update
202
+ x = @game.man_x - @left_window
203
+ y = @game.man_y - @top_window
204
+ display [[x,y], [x+1,y], [x-1,y], [x,y+1], [x,y-1]]
205
+ end
206
+
96
207
  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
208
+ x = @game.man_x - @left_window
209
+ y = @game.man_y - @top_window
210
+ display [[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]]
211
+ end
212
+
213
+ # If the man goes offscreen, do an autoscrolling.
214
+ # @return [Boolean]
215
+ def man_goes_offscreen?
216
+ if @game.man_x < @left_window
217
+ scroll_left
218
+ elsif @game.man_x >= @left_window + MAP_WIDTH
219
+ scroll_right
220
+ elsif @game.man_y < @top_window
221
+ scroll_up
222
+ elsif @game.man_y >= @top_window + MAP_HEIGHT
223
+ scroll_down
224
+ else
225
+ return false
104
226
  end
227
+ true
228
+ end
229
+
230
+ def render_cell image, x, y
231
+ @render.copy(@images[image], :to => [@x_bounds[x], @y_bounds[y]])
105
232
  end
106
233
 
107
234
  # Display the initial map on screen.
108
235
  def display_initial
109
- y = 0
110
- @game.map.each do |row|
111
- # find first wall
112
- x = row.index(RSokoban::WALL)
113
- line = row.strip
114
- line.each_char do |char|
115
- display_cell_taking_care_of_outside char, x, y
116
- x += 1
236
+ window.each_with_index do |row, y_coord|
237
+ x_coord = 0
238
+ row.each_char do |char|
239
+ display_cell_taking_care_of_content char, x_coord, y_coord
240
+ x_coord += 1
117
241
  end
118
- y += 1
119
242
  end
120
243
  end
121
244
 
122
- def display_cell_taking_care_of_outside char, x, y
245
+ # Get the game map through a window
246
+ # @todo refactor. All this stuff (#window, @top_window, MAP_WIDTH, etc) don't belong to this
247
+ # class. Maybe it belongs to Map or LayeredMap or a new class named Window ?
248
+ def window
249
+ ret = []
250
+ @game.map_as_array.each_with_index do |row, y|
251
+ if y >= @top_window and y < @top_window + MAP_HEIGHT
252
+ ret.push row[@left_window, MAP_WIDTH]
253
+ end
254
+ end
255
+ ret
256
+ end
257
+
258
+ def window_update
259
+ reset_map
260
+ display_initial
261
+ end
262
+
263
+ def display_cell_taking_care_of_content char, x_coord, y_coord
123
264
  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
265
+ when WALL then render_wall x_coord, y_coord
266
+ when FLOOR then render_cell :floor, x_coord, y_coord
267
+ when CRATE then render_cell :crate, x_coord, y_coord
268
+ when STORAGE then render_cell :store, x_coord, y_coord
269
+ when MAN then display_man_at x_coord, y_coord
270
+ when MAN_ON_STORAGE then display_man_on_storage_at x_coord, y_coord
271
+ when CRATE_ON_STORAGE then render_cell :crate_store, x_coord, y_coord
131
272
  end
132
273
  end
133
274
 
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
275
+ # taking care of neighbors
276
+ # @todo this is truly a poorly design method
277
+ def render_wall x, y
278
+ up_down_left_right = []
279
+ near = [[x,y-1], [x,y+1], [x-1,y], [x+1,y]]
280
+ map = window
281
+ near.each do |xx, yy|
282
+ if map[yy].nil? or yy < 0
283
+ up_down_left_right << false
284
+ elsif map[yy][xx].nil? or xx < 0
285
+ up_down_left_right << false
286
+ elsif map[yy][xx].chr == WALL
287
+ up_down_left_right << true
288
+ else
289
+ up_down_left_right << false
142
290
  end
143
- }
291
+ end
292
+
293
+ case up_down_left_right
294
+ when [false, false, false, false] then render_cell :wall, x, y
295
+ when [true, false, false, false] then render_cell :wall_u, x, y
296
+ when [false, true, false, false] then render_cell :wall_d, x, y
297
+ when [false, false, true, false] then render_cell :wall_l, x, y
298
+ when [false, false, false, true] then render_cell :wall_r, x, y
299
+ when [true, true, false, false] then render_cell :wall_ud, x, y
300
+ when [true, false, true, false] then render_cell :wall_ul, x, y
301
+ when [true, false, false, true] then render_cell :wall_ur, x, y
302
+ when [false, true, true, false] then render_cell :wall_dl, x, y
303
+ when [false, true, false, true] then render_cell :wall_dr, x, y
304
+ when [false, false, true, true] then render_cell :wall_lr, x, y
305
+ when [true, true, true, false] then render_cell :wall_udl, x, y
306
+ when [true, true, false, true] then render_cell :wall_udr, x, y
307
+ when [true, false, true, true] then render_cell :wall_ulr, x, y
308
+ when [false, true, true, true] then render_cell :wall_dlr, x, y
309
+ when [true, true, true, true] then render_cell :wall_udlr, x, y
310
+ end
144
311
  end
145
312
 
146
313
  def display_man_at x, y
147
314
  case @last_move
148
- when :up then @man_up.display_at x, y
149
- when :down then @man_down.display_at x, y
150
- when :left then @man_left.display_at x, y
315
+ when :up then render_cell :man_up, x, y
316
+ when :down then render_cell :man_down, x, y
317
+ when :left then render_cell :man_left, x, y
151
318
  else
152
- @man_right.display_at x, y
319
+ render_cell :man_right, x, y
153
320
  end
154
321
  end
155
322
 
156
323
  def display_man_on_storage_at x, y
157
324
  case @last_move
158
- when :up then @man_store_up.display_at x, y
159
- when :down then @man_store_down.display_at x, y
160
- when :left then @man_store_left.display_at x, y
325
+ when :up then render_cell :man_store_up, x, y
326
+ when :down then render_cell :man_store_down, x, y
327
+ when :left then render_cell :man_store_left, x, y
161
328
  else
162
- @man_store_right.display_at x, y
329
+ render_cell :man_store_right, x, y
163
330
  end
164
331
  end
165
332
 
166
- def init_gui
167
- init_root
168
- init_menu
169
- init_labels
170
- preload_images
171
- init_map
172
- init_buttons
173
- make_binding
174
- end
175
-
176
333
  def init_root
177
334
  @tk_root = TkRoot.new do
178
335
  title "RSokoban " + File.read($RSOKOBAN_PATH + '/VERSION').strip
179
- minsize(400, 400)
180
336
  resizable(false, false)
181
337
  end
182
338
  end
183
339
 
184
- def init_menu
185
- TkOption.add '*tearOff', 0
186
- menubar = TkMenu.new(@tk_root)
187
- @tk_root['menu'] = menubar
188
-
189
- file = TkMenu.new(menubar)
190
- helpm = TkMenu.new(menubar)
191
- menubar.add :cascade, :menu => file, :label => 'File'
192
- menubar.add :cascade, :menu => helpm, :label => 'Help'
193
-
194
- file.add :command, :label => 'Load level', :command => proc{load_level}, :accelerator => 'Ctrl+L'
195
- file.add :command, :label => 'Load set', :command => proc{load_set}
196
- file.add :separator
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'
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'
201
- file.add :separator
202
- file.add :command, :label => 'Quit', :command => proc{exit}
203
-
204
- helpm.add :command, :label => 'Help', :command => proc{help}, :accelerator => 'F1'
205
- helpm.add :separator
206
- helpm.add :command, :label => 'About', :command => proc{about}
207
- end
208
-
209
- def init_labels
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
214
- end
215
- @tk_label_set = TkLabel.new(@tk_frame_label) do
216
- grid('row' => 0, 'column' => 0, 'sticky' => 'w')
217
- end
218
- @tk_label_level = TkLabel.new(@tk_frame_label) do
219
- grid('row'=>1, 'column'=> 0, 'sticky' => 'w')
220
- end
221
- @tk_label_move = TkLabel.new(@tk_frame_label) do
222
- grid('row'=>2, 'column'=>0, 'sticky' => 'w')
223
- end
224
- end
225
-
226
- def reset_labels
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})")
229
- update_move_information
230
- end
231
-
232
- def update_move_information
233
- @tk_label_move.configure('text' => "Move: #{@game.move_number}")
234
- end
235
-
236
- # Build the map of labels. TkBoxs of the game will be displayed on those labels.
237
- def init_map
238
- row_in_grid = 5
239
- (0...16).each {|row_index|
240
- row = []
241
- (0...19).each {|col_index|
242
- label = TkLabel.new(@tk_root) do
243
- grid('row'=> row_in_grid, 'column'=> col_index, 'padx' => 0, 'pady' => 0, 'ipadx' => 0, 'ipady' => 0)
244
- end
245
- label['borderwidth'] = 0
246
- row.push label
247
- }
248
- @tk_map.push row
249
- row_in_grid += 1
250
- }
340
+ # Let FrameRender build an image to render game in.
341
+ # @todo we don't need the @render member.
342
+ def init_map
343
+ @tk_frame_render = FrameRender.new @tk_root, @cell_size
344
+ @render = @tk_frame_render.render
251
345
  reset_map
252
346
  end
253
347
 
254
348
  # Reset all the map with 'outside' tile.
255
- # @todo little improvement : reload @outside image only if there is something else
256
- # in the current map.
257
349
  def reset_map
258
- @tk_map.each_index {|y|
259
- @tk_map[y].each_index {|x| @outside.display_at(x, y) }
260
- }
261
- end
262
-
263
- def init_buttons
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
270
- text 'Undo'
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)
276
- end
277
-
278
- @tk_retry_button = TkButton.new(@tk_frame_button) do
279
- text 'Retry'
280
- grid('row'=> 0, 'column'=> 2)
281
- end
282
-
283
- @tk_level_button = TkButton.new(@tk_frame_button) do
284
- text 'Level'
285
- grid('row'=> 0, 'column'=> 3)
286
- end
287
-
288
- @tk_next_level_button = TkButton.new(@tk_frame_button) do
289
- text 'Next'
290
- grid('row'=> 0, 'column'=> 4)
291
- end
350
+ width = MAP_WIDTH * @cell_size - 1
351
+ height = MAP_HEIGHT * @cell_size - 1
352
+ @render.copy(@images[:outside], :to => [0, 0, width, height])
292
353
  end
293
354
 
294
355
  # Bind user's actions
295
356
  def make_binding
357
+ @tk_root.bind('Control-Right') { scroll_right }
358
+ @tk_root.bind('Control-Left') { scroll_left }
359
+ @tk_root.bind('Control-Up') { scroll_up }
360
+ @tk_root.bind('Control-Down') { scroll_down }
296
361
  @tk_root.bind('Up') { move :up }
297
362
  @tk_root.bind('Down') { move :down }
298
363
  @tk_root.bind('Left') { move :left }
@@ -303,23 +368,11 @@ module RSokoban::UI
303
368
  @tk_root.bind('Control-l') { load_level }
304
369
  @tk_root.bind('Control-n') { next_level }
305
370
  @tk_root.bind('F1') { help }
306
- @tk_undo_button.command { undo }
307
- @tk_redo_button.command { my_redo }
308
- @tk_retry_button.command { start_level }
309
- @tk_level_button.command { load_level }
310
- @tk_next_level_button.command { next_level }
311
- end
312
-
313
- def undo
314
- result = @game.undo
315
- update_move_information
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
371
+ @tk_frame_button.undo_button.command { undo }
372
+ @tk_frame_button.redo_button.command { my_redo }
373
+ @tk_frame_button.retry_button.command { start_level }
374
+ @tk_frame_button.level_button.command { load_level }
375
+ @tk_frame_button.next_level_button.command { next_level }
323
376
  end
324
377
 
325
378
  # Send the move to Level and process response.
@@ -328,48 +381,172 @@ module RSokoban::UI
328
381
  @last_move = symb
329
382
  result = @game.move symb
330
383
  unless result.error?
331
- update_move_information
384
+ @tk_frame_label.update_move_information @game
332
385
  display_update
333
386
  end
334
387
  if result.win?
335
- response = Tk::messageBox :type => 'yesno', :message => "Level completed !\nPlay next level ?",
388
+ message = if @game.update_record
389
+ "Congratulations ! New record !\n"
390
+ else
391
+ "Level completed !\n"
392
+ end
393
+ # ask for continue ?
394
+ response = Tk::messageBox :type => 'yesno', :message => message + "Play next level ?",
336
395
  :icon => 'question', :title => 'You win !', :parent => @tk_root, :default => 'yes'
337
396
  next_level if response == 'yes'
338
397
  start_level if response == 'no'
339
398
  end
340
399
  end
341
400
 
342
- def preload_images
343
- dir = $RSOKOBAN_PATH + '/skins/default/'
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)
401
+ # @todo Let the Skin class to give the list of image's names.
402
+ def load_skin dir
403
+ skin = Skin.new
404
+ @cell_size = skin.size_of dir
405
+ skin.filenames(dir).each do |key, value|
406
+ @images[key] = TkPhotoImage.new('file' => value)
407
+ end
358
408
  end
359
409
 
360
- def help
361
- HelpDialog.new(@tk_root, "RSokoban Help")
410
+ end
411
+
412
+ # Menu bar attached to tk gui.
413
+ # @since 0.74.1
414
+ class Menu
415
+
416
+ # @param [TkRoot] tk_root
417
+ # @param [TkUI] object_root
418
+ def initialize tk_root, object_root
419
+ TkOption.add '*tearOff', 0
420
+ menubar = TkMenu.new(tk_root)
421
+ menubar[:font] = 'TkMenuFont'
422
+ tk_root['menu'] = menubar
423
+
424
+ file = TkMenu.new(menubar)
425
+ file[:font] = 'TkMenuFont'
426
+ window = TkMenu.new(menubar)
427
+ window[:font] = 'TkMenuFont'
428
+ options = TkMenu.new(menubar)
429
+ options[:font] = 'TkMenuFont'
430
+ helpm = TkMenu.new(menubar)
431
+ helpm[:font] = 'TkMenuFont'
432
+ menubar.add :cascade, :menu => file, :label => 'File'
433
+ menubar.add :cascade, :menu => window, :label => 'Window'
434
+ menubar.add :cascade, :menu => options, :label => 'Options'
435
+ menubar.add :cascade, :menu => helpm, :label => 'Help'
436
+
437
+ file.add :command, :label => 'Load level', :command => proc{object_root.load_level}, :accelerator => 'Ctrl+L'
438
+ file.add :command, :label => 'Load set', :command => proc{object_root.load_set}
439
+ file.add :separator
440
+ file.add :command, :label => 'Undo', :command => proc{object_root.undo}, :accelerator => 'Ctrl+Z'
441
+ file.add :command, :label => 'Redo', :command => proc{object_root.my_redo}, :accelerator => 'Ctrl+Y'
442
+ file.add :command, :label => 'Restart level', :command => proc{object_root.start_level}, :accelerator => 'Ctrl+R'
443
+ file.add :command, :label => 'Next level', :command => proc{object_root.next_level}, :accelerator => 'Ctrl+N'
444
+ file.add :separator
445
+ file.add :command, :label => 'Quit', :command => proc{exit}
446
+
447
+ window.add :command, :label => 'Scroll left', :command => proc{object_root.scroll_left}, :accelerator => 'Ctrl+Left'
448
+ window.add :command, :label => 'Scroll right', :command => proc{object_root.scroll_right}, :accelerator => 'Ctrl+Right'
449
+ window.add :command, :label => 'Scroll up', :command => proc{object_root.scroll_up}, :accelerator => 'Ctrl+Up'
450
+ window.add :command, :label => 'Scroll down', :command => proc{object_root.scroll_down}, :accelerator => 'Ctrl+Down'
451
+
452
+ options.add :command, :label => 'Change skin...', :command => proc{object_root.select_skin}
453
+
454
+ helpm.add :command, :label => 'Help', :command => proc{object_root.help}, :accelerator => 'F1'
455
+ helpm.add :separator
456
+ helpm.add :command, :label => 'About', :command => proc{object_root.about}
362
457
  end
363
458
 
364
- def about
365
- text = "RSokoban #{File.read($RSOKOBAN_PATH + '/VERSION').strip} \n"
366
- text += "This is free software !\n"
367
- text += "Copyright 2011, Xavier Nayrac\n"
368
- text += "Licensed under the GPL-3\n"
369
- text += "Contact: xavier.nayrac@gmail.com"
370
- Tk::messageBox :message => text, :title => 'About'
459
+ end
460
+
461
+ # @since 0.75
462
+ class FrameRender
463
+ attr_reader :render
464
+
465
+ def initialize tk_root, cell_size
466
+ @frame = Tk::Tile::Frame.new(tk_root) do
467
+ grid(:row => 2, :column => 0, :sticky => 'w')
468
+ padding 5
469
+ end
470
+
471
+ @render_label = TkLabel.new(@frame) do
472
+ grid(:row => 0, :column => 0, :padx => 0, :pady => 0, :ipadx => 0, :ipady => 0)
473
+ end
474
+ @render = TkPhotoImage.new(:height => TkUI::MAP_HEIGHT * cell_size, :width => TkUI::MAP_WIDTH * cell_size)
475
+ @render_label .configure(:image => @render)
476
+ end
477
+
478
+ def geometry width_in_cells, height_in_cells, cell_size
479
+ @render[:height] = height_in_cells * cell_size
480
+ @render[:width] = width_in_cells * cell_size
481
+ end
482
+
483
+ end
484
+
485
+ # I am a frame attached to tk gui. I display information through some labels.
486
+ # @since 0.74.1
487
+ class FrameOfLabels
488
+
489
+ def initialize tk_root
490
+ @frame = Tk::Tile::Frame.new(tk_root) do
491
+ grid(:row => 0, :column => 0, :sticky => 'w')
492
+ padding 5
493
+ end
494
+ @label_set = Tk::Tile::Label.new(@frame) do
495
+ grid(:row => 0, :column => 0, :sticky => 'w')
496
+ end
497
+ @label_level = Tk::Tile::Label.new(@frame) do
498
+ grid(:row => 1, :column => 0, :sticky => 'w')
499
+ end
500
+ @label_move = Tk::Tile::Label.new(@frame) do
501
+ grid(:row => 2, :column => 0, :sticky => 'w')
502
+ end
503
+ end
504
+
505
+ def reset_labels game
506
+ @label_set.configure('text' => "Set: #{game.set_title}")
507
+ @label_level.configure('text' => "Level: #{game.level_title} (#{game.level_number}/#{game.set_size}) Record: #{game.record}")
508
+ update_move_information game
509
+ end
510
+
511
+ def update_move_information game
512
+ @label_move.configure('text' => "Move: #{game.move_number}")
371
513
  end
372
514
 
373
515
  end
374
516
 
517
+ # @since 0.74.1
518
+ class FrameOfButtons
519
+ attr_reader :undo_button, :redo_button, :retry_button, :level_button, :next_level_button
520
+
521
+ def initialize tk_root
522
+ @frame = Tk::Tile::Frame.new(tk_root) do
523
+ grid(:row => 1, :column => 0, :sticky => 'w')
524
+ padding 5
525
+ end
526
+ @undo_button = Tk::Tile::Button.new(@frame) do
527
+ text 'Undo'
528
+ grid(:row => 0, :column => 0)
529
+ end
530
+ @redo_button = Tk::Tile::Button.new(@frame) do
531
+ text 'Redo'
532
+ grid(:row => 0, :column => 1)
533
+ end
534
+
535
+ @retry_button = Tk::Tile::Button.new(@frame) do
536
+ text 'Retry'
537
+ grid(:row => 0, :column => 2)
538
+ end
539
+
540
+ @level_button = Tk::Tile::Button.new(@frame) do
541
+ text 'Level'
542
+ grid(:row => 0, :column => 3)
543
+ end
544
+
545
+ @next_level_button = Tk::Tile::Button.new(@frame) do
546
+ text 'Next'
547
+ grid(:row => 0, :column => 4)
548
+ end
549
+ end
550
+ end
551
+
375
552
  end