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.
- data/NEWS +22 -0
- data/README.rdoc +19 -19
- data/RSokoban-0.73.gem +0 -0
- data/TODO +25 -18
- data/VERSION +1 -0
- data/bin/rsokoban +65 -1
- data/lib/rsokoban/exception.rb +4 -0
- data/lib/rsokoban/game.rb +42 -29
- data/lib/rsokoban/level.rb +67 -30
- data/lib/rsokoban/level_loader.rb +4 -4
- data/lib/rsokoban/level_set.rb +20 -10
- data/lib/rsokoban/map.rb +51 -0
- data/lib/rsokoban/move_recorder.rb +38 -0
- data/lib/rsokoban/option.rb +29 -12
- data/lib/rsokoban/raw_level.rb +15 -4
- data/lib/rsokoban/ui/base_ui.rb +37 -0
- data/lib/rsokoban/ui/console.rb +43 -33
- data/lib/rsokoban/ui/curses_console.rb +45 -36
- data/lib/rsokoban/ui/player_action.rb +74 -0
- data/lib/rsokoban/ui/tk_ui.rb +474 -0
- data/lib/rsokoban/ui.rb +10 -0
- data/lib/rsokoban.rb +4 -5
- data/skins/default/crate.bmp +0 -0
- data/skins/default/crate_store.bmp +0 -0
- data/skins/default/floor.bmp +0 -0
- data/skins/default/man_down.bmp +0 -0
- data/skins/default/man_left.bmp +0 -0
- data/skins/default/man_right.bmp +0 -0
- data/skins/default/man_store_down.bmp +0 -0
- data/skins/default/man_store_left.bmp +0 -0
- data/skins/default/man_store_right.bmp +0 -0
- data/skins/default/man_store_up.bmp +0 -0
- data/skins/default/man_up.bmp +0 -0
- data/skins/default/outside.bmp +0 -0
- data/skins/default/readme +1 -0
- data/skins/default/store.bmp +0 -0
- data/skins/default/wall.bmp +0 -0
- data/test/original.xsb +0 -2
- data/test/tc_level.rb +37 -37
- data/test/tc_level_loader.rb +14 -2
- data/test/tc_level_set.rb +8 -0
- data/test/tc_map.rb +40 -0
- data/test/tc_move_recorder.rb +100 -0
- data/test/tc_raw_level.rb +5 -5
- data/test/test.rb +3 -1
- data/test/test_file2.xsb +2 -0
- data/test/ui/tc_console.rb +27 -13
- data/test/ui/tc_player_action.rb +156 -0
- metadata +38 -25
- 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
|
data/lib/rsokoban/ui.rb
ADDED
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
|
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
|