RSokoban 0.71

Sign up to get free protection for your applications and to get access to all the features.
data/data/test.xsb ADDED
@@ -0,0 +1,30 @@
1
+ ; another test file
2
+
3
+ ; 6 levels
4
+ ; blablabla
5
+ ; bla
6
+
7
+ #####
8
+ #.$@#
9
+ #####
10
+ ; 1
11
+ ; blabla
12
+
13
+ ######
14
+ #. $@#
15
+ ######
16
+ ; 2
17
+
18
+ #####
19
+ # #
20
+ #$ #
21
+ #. @#
22
+ #####
23
+ ; 3
24
+
25
+ ######
26
+ #.$ #
27
+ #.$ #
28
+ #.$ @#
29
+ ######
30
+ ; 4
@@ -0,0 +1,16 @@
1
+ require "rsokoban/moveable"
2
+
3
+ module RSokoban
4
+
5
+ # I am a moveable crate.
6
+ class Crate < Position
7
+ include RSokoban::Moveable
8
+
9
+ # @param [Fixnum] x la coordonnée x
10
+ # @param [Fixnum] y la coordonnée y
11
+ def initialize x, y
12
+ super(x, y)
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,7 @@
1
+ module RSokoban
2
+ class NoFileError < ArgumentError
3
+ end
4
+
5
+ class LevelNumberTooHighError < ArgumentError
6
+ end
7
+ end
@@ -0,0 +1,105 @@
1
+ module RSokoban
2
+
3
+ # I am mostly the game loop.
4
+ class Game
5
+
6
+ # Construct a new game that you can later run.
7
+ # @param [:curses|:portable] ui_as_symbol the user interface for the game
8
+ def initialize ui_as_symbol
9
+ @levelLoader = LevelLoader.new "original.xsb"
10
+ @levelNumber = 1
11
+ case ui_as_symbol
12
+ when :curses
13
+ require "rsokoban/ui/curses_console"
14
+ @ui = UI::CursesConsole.new
15
+ when :portable
16
+ require "rsokoban/ui/console"
17
+ @ui = UI::Console.new
18
+ end
19
+ end
20
+
21
+ # I am the game loop.
22
+ def run
23
+ action = start_level
24
+ loop do
25
+ if action.is_a?(Fixnum)
26
+ action = load_level action
27
+ next
28
+ elsif action.instance_of?(String)
29
+ # Assuming we recieve a filename of level's set to load
30
+ action = load_a_new_set action
31
+ next
32
+ elsif action == :quit
33
+ break
34
+ elsif action == :next
35
+ action = next_level
36
+ next
37
+ elsif action == :retry
38
+ action = try_again
39
+ next
40
+ elsif [:down, :up, :left, :right].include?(action)
41
+ result = @level.move(action)
42
+ end
43
+
44
+ if result.start_with?('WIN')
45
+ action = @ui.get_action('WIN', @level.picture, result)
46
+ else
47
+ action = @ui.get_action('DISPLAY', @level.picture, result)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Load and start the next level of the set
55
+ # @return [Object] the user's {action}[Console#get_action]
56
+ def next_level
57
+ @levelNumber += 1
58
+ start_level
59
+ end
60
+
61
+ # Restart the current level
62
+ # @return [Object] the user's {action}[Console#get_action]
63
+ def try_again
64
+ start_level
65
+ end
66
+
67
+ # Load a new set of levels and start its first level.
68
+ # @param [String] setname the name of the set (with .xsb extension)
69
+ # @return [Object] the user's {action}[Console#get_action]
70
+ def load_a_new_set setname
71
+ begin
72
+ @levelLoader = LevelLoader.new setname
73
+ @levelNumber = 1
74
+ @level = @levelLoader.level(@levelNumber)
75
+ message = "Level : #{@level.title}"
76
+ rescue NoFileError
77
+ message = "Error, no such file : #{setname}"
78
+ end
79
+ @ui.get_action('START', @level.picture, message)
80
+ end
81
+
82
+ # Load a level from the current set.
83
+ # @param [Fixnum] num the number of the set (base 1)
84
+ # @return [Object] the user's {action}[Console#get_action]
85
+ def load_level num
86
+ @levelNumber = num
87
+ start_level
88
+ end
89
+
90
+ # Start a level, according to some instance members.
91
+ # @return [Object] the user's {action}[Console#get_action]
92
+ def start_level
93
+ begin
94
+ @level = @levelLoader.level(@levelNumber)
95
+ @ui.get_action('START', @level.picture, "Level : #{@level.title}")
96
+ rescue LevelNumberTooHighError
97
+ @ui.get_action('END_OF_SET', ['####'], "No more levels in this set")
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+
104
+
105
+ end
@@ -0,0 +1,269 @@
1
+ module RSokoban
2
+
3
+ # I am a level of the game.
4
+ # To complete a level, place each crate ('$') on a storage location ('.').
5
+ class Level
6
+ attr_reader :floor, :man, :crates, :storages, :title
7
+
8
+ # I build the level from a RawLevel object.
9
+ # @example a RawLevel object
10
+ # A RawLevel object have got one title and one 'picture'. A 'picture' is an array of string.
11
+ # Each string contain one line of the level map.
12
+ # 'Level 1', ['#####', '#.o@#', '#####']
13
+ #
14
+ # * '#' is a wal
15
+ # * '.' is a storage location
16
+ # * '$' is a crate
17
+ # * '*' is a crate on storage location
18
+ # * '@' is the man
19
+ # * ' ' is an empty floor
20
+ #
21
+ # @param [RawLevel] rawLevel
22
+ def initialize rawLevel
23
+ @title = rawLevel.title
24
+ @floor = init_floor rawLevel.picture
25
+ @man = init_man rawLevel.picture
26
+ @crates = []
27
+ @storages = []
28
+ init_crates_and_storages rawLevel.picture
29
+ @move = 0
30
+ @picture = nil
31
+ end
32
+
33
+ # Two Level objects are equals if their @title, @floor, @man, @crates and @storages are equals.
34
+ # @param [Object] obj
35
+ # @return [false|true]
36
+ def ==(obj)
37
+ return false unless obj.kind_of?(Level)
38
+ @floor == obj.floor and @man == obj.man and @crates == obj.crates and @storages == obj.storages and @title == obj.title
39
+ end
40
+
41
+ # Synonym of #==
42
+ # @see #==
43
+ def eql?(obj)
44
+ self == obj
45
+ end
46
+
47
+ # Get an instant picture of the game.
48
+ # @return [Array<String>] the picture, after X turns of game.
49
+ def picture
50
+ @picture = init_floor @floor
51
+ draw_crates
52
+ draw_storages
53
+ draw_man
54
+ @picture
55
+ end
56
+
57
+ # Move the man one box up.
58
+ # @return [String] the move's result
59
+ # ["ERROR wall"] if the player is stopped by a wall
60
+ # ['ERROR wall behind crate'] si le joueur est stoppé par une caisse suivie d'un mur
61
+ # ['ERROR double crate'] if the player is stopped by a crate followed by a wall
62
+ # ['OK move ?'] if the move is accepted (? is replaced by the number of the move)
63
+ # ['WIN move ?'] if the level is completed (? is replaced by the number of the move)
64
+ def moveUp
65
+ move :up
66
+ end
67
+
68
+ # Move the man one box down.
69
+ # @see #moveUp for more explanation
70
+ def moveDown
71
+ move :down
72
+ end
73
+
74
+ # Move the man one box left.
75
+ # @see #moveUp for more explanation
76
+ def moveLeft
77
+ move :left
78
+ end
79
+
80
+ # Move the man one box right.
81
+ # @see #moveUp for more explanation
82
+ def moveRight
83
+ move :right
84
+ end
85
+
86
+ # Move the man one box +direction+.
87
+ # @see #moveUp for more explanation
88
+ def move direction
89
+ return 'ERROR wall' if wall?(direction)
90
+ return 'ERROR wall behind crate' if wall_behind_crate?(direction)
91
+ return 'ERROR double crate' if double_crate?(direction)
92
+ @move += 1
93
+ @man.send(direction)
94
+ if @crates.include?(Crate.new(@man.x, @man.y))
95
+ i = @crates.index(Crate.new(@man.x, @man.y))
96
+ @crates[i].send(direction)
97
+ end
98
+ return "WIN move #{@move}" if win?
99
+ "OK move #{@move}"
100
+ end
101
+
102
+ private
103
+
104
+ # @return true if all crates are on a storage location
105
+ def win?
106
+ return false if @crates.size == 0 # needed for testing purpose.
107
+ @crates.each {|c|
108
+ return false unless @storages.include?(c)
109
+ }
110
+ true
111
+ end
112
+
113
+ # Is there a wall near the man, in the direction pointed to by +direction+ ?
114
+ # @param [:up|:down|:left|:right] direction
115
+ # @return [true|false]
116
+ def wall? direction
117
+ case direction
118
+ when :up
119
+ box = what_is_on(@man.x, @man.y-1)
120
+ when :down
121
+ box = what_is_on(@man.x, @man.y+1)
122
+ when :left
123
+ box = what_is_on(@man.x-1, @man.y)
124
+ when :right
125
+ box = what_is_on(@man.x+1, @man.y)
126
+ end
127
+ return(box == WALL)
128
+ end
129
+
130
+ # Is there a crate followed by a wall near the man, in the direction pointed to by +direction+ ?
131
+ # @param [:up|:down|:left|:right] direction
132
+ # @return [true|false]
133
+ def wall_behind_crate?(direction)
134
+ case direction
135
+ when :up
136
+ near = crate?(@man.x, @man.y-1)
137
+ boxBehind = what_is_on(@man.x, @man.y-2)
138
+ when :down
139
+ near = crate?(@man.x, @man.y+1)
140
+ boxBehind = what_is_on(@man.x, @man.y+2)
141
+ when :left
142
+ near = crate?(@man.x-1, @man.y)
143
+ boxBehind = what_is_on(@man.x-2, @man.y)
144
+ when :right
145
+ near = crate?(@man.x+1, @man.y)
146
+ boxBehind = what_is_on(@man.x+2, @man.y)
147
+ end
148
+ return(near and boxBehind == WALL)
149
+ end
150
+
151
+ # Is there a crate followed by a crate near the man, in the direction pointed to by +direction+ ?
152
+ # @param [:up|:down|:left|:right] direction
153
+ # @return [true|nil]
154
+ def double_crate?(direction)
155
+ case direction
156
+ when :up
157
+ true if crate?(@man.x, @man.y-1) and crate?(@man.x, @man.y-2)
158
+ when :down
159
+ true if crate?(@man.x, @man.y+1) and crate?(@man.x, @man.y+2)
160
+ when :left
161
+ true if crate?(@man.x-1, @man.y) and crate?(@man.x-2, @man.y)
162
+ when :right
163
+ true if crate?(@man.x+1, @man.y) and crate?(@man.x+2, @man.y)
164
+ end
165
+ end
166
+
167
+ # Is there a crate ('o' or '*') in the box pointed to by x, y ?
168
+ # @param [Fixnum] x x coordinate in the map
169
+ # @param [Fixnum] y y coordinate in the map
170
+ # @return [true|false]
171
+ def crate?(x, y)
172
+ box = what_is_on(x, y)
173
+ box == CRATE or box == CRATE_ON_STORAGE
174
+ end
175
+
176
+ # Draw the man for @picture output
177
+ def draw_man
178
+ box = what_is_on @man.x, @man.y
179
+ put_man_in_picture if box == FLOOR
180
+ put_man_on_storage_in_picture if box == STORAGE
181
+ end
182
+
183
+ def put_man_in_picture
184
+ @picture[@man.y][@man.x] = MAN
185
+ end
186
+
187
+ def put_man_on_storage_in_picture
188
+ @picture[@man.y][@man.x] = MAN_ON_STORAGE
189
+ end
190
+
191
+ # Draw the crates for @picture output
192
+ def draw_crates
193
+ @crates.each {|crate| @picture[crate.y][crate.x] = what_is_on(crate.x, crate.y) }
194
+ end
195
+
196
+ # Draw the storages location for @picture output
197
+ def draw_storages
198
+ @storages.each {|st| @picture[st.y][st.x] = what_is_on(st.x, st.y) }
199
+ end
200
+
201
+ # Get the content of box x, y
202
+ # @param [Fixnum] x x coordinate in the map
203
+ # @param [Fixnum] y y coordinate in the map
204
+ # @return [' ' | '#' | '.' | 'o' | '*']
205
+ def what_is_on x, y
206
+ box = (@floor[y][x]).chr
207
+ if box == FLOOR
208
+ s = Storage.new(x, y)
209
+ c = Crate.new(x, y)
210
+ if @storages.include?(s) and @crates.include?(c)
211
+ box = CRATE_ON_STORAGE
212
+ elsif @storages.include?(s)
213
+ box = STORAGE
214
+ elsif @crates.include?(c)
215
+ box = CRATE
216
+ end
217
+ end
218
+ box
219
+ end
220
+
221
+ # Removes all storages locations, all crates and the man, leaving only walls and floor.
222
+ #
223
+ # @param [Array<String>] picture
224
+ # @return [Array<String>] picture with only walls and floor
225
+ def init_floor picture
226
+ floor = []
227
+ picture.each {|x| floor.push x.tr("#{STORAGE}#{CRATE}#{MAN}#{CRATE_ON_STORAGE}", FLOOR) }
228
+ floor
229
+ end
230
+
231
+ # Find the man's position, at the begining of the level.
232
+ #
233
+ # @param [Array<String>] picture
234
+ # @return [Man] an initialised man
235
+ def init_man picture
236
+ x = y = 0
237
+ picture.each {|line|
238
+ if line.include?(MAN)
239
+ x = line.index(MAN)
240
+ break
241
+ end
242
+ y += 1
243
+ }
244
+ Man.new x, y
245
+ end
246
+
247
+ # Find position of crates and storages, at the begining of the level.
248
+ #
249
+ # @param [Array<String>] picture
250
+ def init_crates_and_storages picture
251
+ y = 0
252
+ picture.each do |line|
253
+ count = 0
254
+ line.each_char do |c|
255
+ @crates.push Crate.new(count, y) if c == CRATE
256
+ @storages.push Storage.new(count, y) if c == STORAGE
257
+ if c == CRATE_ON_STORAGE
258
+ @crates.push Crate.new(count, y)
259
+ @storages.push Storage.new(count, y)
260
+ end
261
+ count += 1
262
+ end
263
+ y += 1
264
+ end
265
+ end
266
+
267
+ end
268
+
269
+ end
@@ -0,0 +1,58 @@
1
+ module RSokoban
2
+
3
+ # Je charge le fichier contenant les niveaux du jeu. À ma création, vous m'indiquez un fichier
4
+ # au format xsb contenant un ou plusieurs niveaux.
5
+ # Vous pouvez ensuite me demander un niveau particulier (un objet Level).
6
+ # @todo translate, document and refactor
7
+ class LevelLoader
8
+ attr_reader :level, :set
9
+
10
+ # @filename [String] le nom du fichier où trouver les niveaux.
11
+ # Ce fichier est cherché dans le dossier data/.
12
+ # @throws [RSokoban::NoFileError] si le fichier n'existe pas
13
+ # @see LevelSet for an overview of .xsb file format
14
+ def initialize filename
15
+ filename = "#{$RSOKOBAN_DATA_PATH}/" + filename
16
+ raise NoFileError unless File.exist?(filename)
17
+ @set = LevelSet.new
18
+ file = open(filename)
19
+ @set.title = file.readline.chomp.sub(/;/, '').strip
20
+ file.readline # must be blank line
21
+ line = file.readline
22
+ desc = ''
23
+ while line[0] == ?; do
24
+ desc += line.sub(/;/, '').sub(/\s*/, '')
25
+ line = file.readline
26
+ end
27
+ @set.description = desc
28
+
29
+ loop do
30
+ begin
31
+ line = file.readline.chomp
32
+ raw = []
33
+ while line =~ /^ *#/
34
+ raw.push line
35
+ line = file.readline.chomp
36
+ end
37
+ line = line.chomp.sub(/;/, '').sub(/\s*/, '')
38
+ @set.rawLevels.push RawLevel.new(line, raw)
39
+
40
+ line = file.readline
41
+ while line[0, 1] == ';'
42
+ line = file.readline
43
+ end
44
+ # must be a blank line here
45
+ rescue EOFError
46
+ break
47
+ end
48
+ end
49
+ end
50
+
51
+ def level num
52
+ raise LevelNumberTooHighError if num > @set.rawLevels.size
53
+ Level.new @set.rawLevels[num-1]
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,31 @@
1
+ module RSokoban
2
+
3
+ # I am a set of sokoban levels.
4
+ # Level set are found in .xsb files.
5
+ #
6
+ # =xsb file format
7
+ #
8
+ # Info lines begins with semi-colon (;)
9
+ # Map lines begins with a # (that's a wall !)
10
+ #
11
+ # 1: First info is title of the set
12
+ # 2: Blank line
13
+ # 3: List of info lines : description
14
+ # 4: Blank line
15
+ # 5: Level map
16
+ # 6: info title of this level
17
+ # 7: List of info lines : blabla about this level
18
+ #
19
+ # From 4 to 7 again for each supplementary level
20
+ class LevelSet
21
+ attr_accessor :title, :description, :rawLevels
22
+
23
+ def initialize
24
+ @title = 'Unknown set title'
25
+ @description = 'Empty description'
26
+ @rawLevels = []
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,17 @@
1
+ require "rsokoban/moveable"
2
+
3
+ module RSokoban
4
+
5
+ # Je suis le bonhomme qui pousse les caisses...
6
+ class Man < Position
7
+ include RSokoban::Moveable
8
+
9
+ # @param [Fixnum] x la coordonnée x
10
+ # @param [Fixnum] y la coordonnée y
11
+ def initialize x, y
12
+ super(x, y)
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,22 @@
1
+ module RSokoban
2
+
3
+ # Permits to move one box in all 4 directions.
4
+ module Moveable
5
+ def up
6
+ @y -= 1
7
+ end
8
+
9
+ def down
10
+ @y += 1
11
+ end
12
+
13
+ def left
14
+ @x -= 1
15
+ end
16
+
17
+ def right
18
+ @x += 1
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,93 @@
1
+ require 'optparse'
2
+
3
+ # I parse the command line.
4
+ class Option
5
+
6
+ # Here is a list of command line options :
7
+ # * --curses
8
+ # * --version
9
+ # * --license
10
+ # * --help
11
+ # * --help-output
12
+ # * --portable
13
+ # @todo refactoring
14
+ def initialize
15
+ @options = {:ui => :curses}
16
+
17
+ optparse = OptionParser.new do|opts|
18
+ opts.banner = "Usage: #{$0} [options]"
19
+
20
+ opts.on( '-c', '--curses', 'Use curses console for user interface (default)' ) do
21
+ @options[:ui] = :curses
22
+ end
23
+
24
+ @options[:help_output] = false
25
+ opts.on( '-o', '--help-output', 'Print help on output options and exit' ) do
26
+ @options[:help_output] = true
27
+ end
28
+
29
+ @options[:license] = false
30
+ opts.on( '-l', '--license', 'Print program\'s license and exit' ) do
31
+ @options[:license] = true
32
+ end
33
+
34
+ opts.on( '-p', '--portable', 'Use standard console for user interface' ) do
35
+ @options[:ui] = :portable
36
+ end
37
+
38
+ @options[:version] = false
39
+ opts.on( '-v', '--version', 'Print version number and exit' ) do
40
+ @options[:version] = true
41
+ end
42
+
43
+ opts.on( '-h', '--help', 'Display this screen' ) do
44
+ puts opts
45
+ exit
46
+ end
47
+ end
48
+
49
+ begin
50
+ optparse.parse!
51
+ rescue OptionParser::InvalidOption => e
52
+ puts e.to_s
53
+ exit 1
54
+ end
55
+
56
+ print_version if @options[:version]
57
+ print_license if @options[:license]
58
+ print_help_output if @options[:help_output]
59
+ end
60
+
61
+ def [](k)
62
+ @options[k]
63
+ end
64
+
65
+ private
66
+
67
+ def print_version
68
+ puts RSokoban::VERSION
69
+ exit
70
+ end
71
+
72
+ def print_license
73
+ puts "RSokoban is licensed under the GPL 3. See the COPYING's file."
74
+ exit
75
+ end
76
+
77
+ def print_help_output
78
+ help=<<EOS
79
+
80
+ RSokoban can use 2 user interfaces :
81
+
82
+ --curses
83
+ This is the default UI. It uses the curses library in a console window.
84
+ It works on Linux. I don't know if it works on Windows or OSX.
85
+
86
+ --portable
87
+ It uses a plain console window. This UI is boring but should work
88
+ everywhere.
89
+ EOS
90
+ puts help
91
+ exit
92
+ end
93
+ end
@@ -0,0 +1,24 @@
1
+ module RSokoban
2
+
3
+ # I represent an x,y coordinate.
4
+ # @todo document
5
+ class Position
6
+ attr_reader :x, :y
7
+
8
+ def initialize x = 0, y = 0
9
+ @x = x
10
+ @y = y
11
+ end
12
+
13
+ def ==(obj)
14
+ return false unless obj.kind_of?(Position)
15
+ @x == obj.x and @y == obj.y
16
+ end
17
+
18
+ def eql?(obj)
19
+ self == obj
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,16 @@
1
+ module RSokoban
2
+
3
+ # I figure out a level in a very simple format.
4
+ # I have a title and an array of string (named +picture+), representing the map.
5
+ # @todo document and give some examples
6
+ class RawLevel
7
+ attr_accessor :title, :picture
8
+
9
+ def initialize title = 'Unknown level title', picture = []
10
+ @title = title
11
+ @picture = picture
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,14 @@
1
+ module RSokoban
2
+
3
+ # Je suis un emplacement de stockage, c'est à dire un endroit où il faut placer une caisse
4
+ # pour gagner le niveau.
5
+ class Storage < Position
6
+
7
+ # @param [Fixnum] x la coordonnée x
8
+ # @param [Fixnum] y la coordonnée y
9
+ def initialize x, y
10
+ super(x, y)
11
+ end
12
+ end
13
+
14
+ end