RSokoban 0.71

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/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