RSokoban 0.73 → 0.74

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,14 @@ module RSokoban
5
5
  class Level
6
6
  attr_reader :floor, :man, :crates, :storages, :title
7
7
 
8
+ # Get map width of this level, in cells
9
+ # @return [Fixnum]
10
+ attr_reader :width
11
+
12
+ # Get map height of this level, in cells
13
+ # @return [Fixnum]
14
+ attr_reader :height
15
+
8
16
  # I build the level from a RawLevel object.
9
17
  # @example a RawLevel object
10
18
  # A RawLevel object have got one title and one 'picture'. A 'picture' is an array of string.
@@ -21,6 +29,7 @@ module RSokoban
21
29
  # @param [RawLevel] rawLevel
22
30
  def initialize rawLevel
23
31
  @title = rawLevel.title
32
+ init_dimension rawLevel.map
24
33
  @floor = init_floor rawLevel.map
25
34
  @man = init_man rawLevel.map
26
35
  @crates = []
@@ -55,43 +64,14 @@ module RSokoban
55
64
  @map
56
65
  end
57
66
 
58
- # Move the man one box up.
59
- # @return [String] the move's result
60
- # ["ERROR wall"] if the player is stopped by a wall
61
- # ['ERROR wall behind crate'] si le joueur est stoppé par une caisse suivie d'un mur
62
- # ['ERROR double crate'] if the player is stopped by a crate followed by a wall
63
- # ['OK move ?'] if the move is accepted (? is replaced by the number of the move)
64
- # ['WIN move ?'] if the level is completed (? is replaced by the number of the move)
65
- def moveUp
66
- move :up
67
- end
68
-
69
- # Move the man one box down.
70
- # @see #moveUp for more explanation
71
- def moveDown
72
- move :down
73
- end
74
-
75
- # Move the man one box left.
76
- # @see #moveUp for more explanation
77
- def moveLeft
78
- move :left
79
- end
80
-
81
- # Move the man one box right.
82
- # @see #moveUp for more explanation
83
- def moveRight
84
- move :right
85
- end
86
-
87
67
  # Move the man one box +direction+.
88
- # @see #moveUp for more explanation
68
+ # @param [:up|:down|:right|:left] direction
69
+ # @return [MoveResult] the move's result
89
70
  def move direction
90
- return 'ERROR wall' if wall?(direction)
91
- return 'ERROR wall behind crate' if wall_behind_crate?(direction)
92
- return 'ERROR double crate' if double_crate?(direction)
71
+ return MoveResult.new(:status => :error, :message => 'wall') if wall?(direction)
72
+ return MoveResult.new(:status => :error, :message => 'wall behind crate') if wall_behind_crate?(direction)
73
+ return MoveResult.new(:status => :error, :message => 'double crate') if double_crate?(direction)
93
74
  @move += 1
94
-
95
75
  @man.send(direction)
96
76
  if @crates.include?(Crate.new(@man.x, @man.y))
97
77
  i = @crates.index(Crate.new(@man.x, @man.y))
@@ -100,8 +80,31 @@ module RSokoban
100
80
  else
101
81
  @move_recorder.record direction
102
82
  end
103
- return "WIN move #{@move}" if win?
104
- "OK move #{@move}"
83
+
84
+ return MoveResult.new(:status => :win, :move_number => @move) if win?
85
+ MoveResult.new(:status => :ok, :move_number => @move)
86
+ end
87
+
88
+ # Redo the last undo
89
+ # @since 0.74
90
+ def redo
91
+ begin
92
+ case @move_recorder.redo
93
+ when :up, :UP then direction = :up
94
+ when :down, :DOWN then direction = :down
95
+ when :left, :LEFT then direction = :left
96
+ when :right, :RIGHT then direction = :right
97
+ end
98
+ @man.send(direction)
99
+ @move += 1
100
+ if @crates.include?(Crate.new(@man.x, @man.y))
101
+ i = @crates.index(Crate.new(@man.x, @man.y))
102
+ @crates[i].send(direction)
103
+ end
104
+ rescue EmptyRedoError
105
+ # Nothing to do
106
+ end
107
+ MoveResult.new(:status => :ok, :move_number => @move)
105
108
  end
106
109
 
107
110
  # Undo last move
@@ -133,7 +136,14 @@ module RSokoban
133
136
  rescue EmptyMoveQueueError
134
137
  # Nothing to do
135
138
  end
136
- "OK move #{@move}"
139
+ MoveResult.new(:status => :ok, :move_number => @move)
140
+ end
141
+
142
+ # Get current move number
143
+ # @return [Fixnum]
144
+ # @since 0.74
145
+ def move_number
146
+ @move
137
147
  end
138
148
 
139
149
  private
@@ -265,6 +275,13 @@ module RSokoban
265
275
  floor
266
276
  end
267
277
 
278
+ # Initialize map width and map height of this level
279
+ def init_dimension map
280
+ @width = 0
281
+ map.each {|y| @width = y.size if y.size > @width }
282
+ @height = map.size
283
+ end
284
+
268
285
  # Find the man's position, at the begining of the level.
269
286
  #
270
287
  # @param [Map] map
@@ -1,27 +1,29 @@
1
1
  module RSokoban
2
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
3
+ # I load a file containing the levels of the game. On instanciation,
4
+ # you tell me a file (in xsb file format) containing one or more levels.
5
+ #
6
+ # You can then ask for a particular level (a Level object). You can use me too
7
+ # to find the description of a level.
8
+ # @todo give some examples
9
+ # @todo document and refactor
7
10
  class LevelLoader
8
- attr_reader :level, :set
9
11
 
10
- # @param [String] filename le nom du fichier où trouver les niveaux.
11
- # Ce fichier est cherché dans le dossier data/.
12
- # @raise [RSokoban::NoFileError] si le fichier n'existe pas
12
+ # @param [String] filename an xsb filename.
13
+ # This file is searched in thedata/ folder.
14
+ # @raise [RSokoban::NoFileError] if filename doesn't exist
13
15
  # @see LevelSet overview of .xsb file format
14
16
  def initialize filename
15
17
  filename = "#{$RSOKOBAN_DATA_PATH}/" + filename
16
18
  raise NoFileError unless File.exist?(filename)
17
19
  @set = LevelSet.new
18
20
  file = open(filename)
19
- @set.title = file.readline.chomp.sub(/;/, '').strip
21
+ @set.title = file.readline.get_xsb_info_line_chomp
20
22
  file.readline # must be blank line
21
23
  line = file.readline
22
24
  desc = ''
23
25
  while line[0] == ?; do
24
- desc += line.sub(/;/, '').sub(/\s*/, '')
26
+ desc += line.get_xsb_info_line
25
27
  line = file.readline
26
28
  end
27
29
  @set.description = desc
@@ -34,7 +36,7 @@ module RSokoban
34
36
  raw.push line
35
37
  line = file.readline.chomp
36
38
  end
37
- line = line.chomp.sub(/;/, '').sub(/\s*/, '')
39
+ line = line.get_xsb_info_line_chomp
38
40
  @set.rawLevels.push RawLevel.new(line, raw) unless raw.empty?
39
41
 
40
42
  line = file.readline
@@ -48,11 +50,32 @@ module RSokoban
48
50
  end
49
51
  end
50
52
 
53
+ # Get the level numbered +num+
54
+ # @param [Fixnum] num a level number in base 1
55
+ # @return [Level]
51
56
  def level num
52
57
  raise LevelNumberTooHighError if num > @set.rawLevels.size
53
58
  Level.new @set.rawLevels[num-1]
54
59
  end
55
60
 
61
+ # Get the description field of the loaded set of levels.
62
+ # @return [String] possibly multi-line
63
+ def file_description
64
+ @set.description
65
+ end
66
+
67
+ # Get number of levels in this set.
68
+ # @return [Fixnum]
69
+ def size
70
+ @set.size
71
+ end
72
+
73
+ # Get title of this set.
74
+ # @return [String]
75
+ def title
76
+ @set.title
77
+ end
78
+
56
79
  end
57
80
 
58
81
  end
@@ -7,14 +7,25 @@ module RSokoban
7
7
 
8
8
  def initialize
9
9
  @queue = []
10
+ @redo = []
10
11
  end
11
12
 
12
- # @return [:up, :down, :left, :right]
13
+ # @return [:up, :down, :left, :right, :UP, :DOWN, :LEFT, :RIGHT]
13
14
  # @raise EmptyMoveQueueError
14
15
  # @since 0.73
15
16
  def undo
16
17
  raise EmptyMoveQueueError if @queue.empty?
17
- @queue.pop
18
+ @redo << @queue.pop
19
+ @redo[-1]
20
+ end
21
+
22
+ # @return [:up, :down, :left, :right, :UP, :DOWN, :LEFT, :RIGHT]
23
+ # @raise EmptyRedoError
24
+ # @since 0.74
25
+ def redo
26
+ raise EmptyRedoError if @redo.empty?
27
+ @queue << @redo.pop
28
+ @queue[-1]
18
29
  end
19
30
 
20
31
  # Record the move +direction+
@@ -23,6 +34,7 @@ module RSokoban
23
34
  # @since 0.73
24
35
  def record direction, push = nil
25
36
  raise ArgumentError unless [:up, :down, :left, :right].include?(direction)
37
+ @redo = []
26
38
  if push
27
39
  @queue.push :UP if direction == :up
28
40
  @queue.push :DOWN if direction == :down
@@ -0,0 +1,37 @@
1
+ module RSokoban
2
+
3
+ # I am the result content of a move.
4
+
5
+ # @since 0.74
6
+ class MoveResult
7
+
8
+ # @param [Hash] hash the result
9
+ # @option hash [Symbol] :status Could be :ok, :win or :error
10
+ # @option hash [Fixnum] :move_number for a status of :ok or :win
11
+ # @option hash [String] :message for a status of :error
12
+ def initialize hash
13
+ @hash = hash
14
+ end
15
+
16
+ def [](k)
17
+ @hash[k]
18
+ end
19
+
20
+ # @return true if move is ok
21
+ def ok?
22
+ @hash[:status] == :ok
23
+ end
24
+
25
+ # @return true if move result to winning the game
26
+ def win?
27
+ @hash[:status] == :win
28
+ end
29
+
30
+ # @return true if move is an error
31
+ def error?
32
+ @hash[:status] == :error
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -13,9 +13,6 @@ module RSokoban::UI
13
13
  # my childs permit the user to do, they can only return an
14
14
  # ActionPlayer object.
15
15
  #
16
- # @param [Hash] hash
17
- # :type => type of message, :win or :start or :display or :end_of_set
18
- # :map => current game map
19
16
  # @param [Hash] hash the options passed to the UI.
20
17
  # @option hash [:win|:start|:display|:end_of_set] :type The type of the message (always +requiered+)
21
18
  # @option hash [Map] :map The current map of the game (always +requiered+)
@@ -47,7 +47,7 @@ module RSokoban::UI
47
47
 
48
48
  def ask_for_next_level
49
49
  printf "Play next level ? "
50
- line = readline.chomp
50
+ line = gets.chomp
51
51
  if ['yes', 'ye', 'y', 'YES', 'YE', 'Y'].include?(line)
52
52
  PlayerAction.new(:next)
53
53
  else
@@ -57,7 +57,7 @@ module RSokoban::UI
57
57
 
58
58
  def ask_player
59
59
  printf "Your choice ? "
60
- line = readline.chomp
60
+ line = gets.chomp
61
61
  response = parse line
62
62
  if response.nil?
63
63
  puts "Error : #{line}"
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  module RSokoban::UI
4
2
 
5
3
  # I am a console user interface using curses library.
@@ -18,7 +18,7 @@ module RSokoban::UI
18
18
 
19
19
  @@Allowed_symbols = [ :up, :down, :left, :right, :quit, :next, :retry, :undo ]
20
20
 
21
- # You can the the {class description}[PlayerAction] for an allowed list of value.
21
+ # You can look to {PlayerAction} for an allowed list of value.
22
22
  # @param [Object] value optional initial action
23
23
  # @raise ArgumentError if value is not allowed
24
24
  def initialize value = nil
@@ -30,8 +30,8 @@ module RSokoban::UI
30
30
  end
31
31
 
32
32
  # Set the player action.
33
- # You can the the {class description}[PlayerAction] for an allowed list of value.
34
33
  # @param [Object] value the player action
34
+ # @raise ArgumentError if value is not allowed
35
35
  def action=(value)
36
36
  if value.instance_of?(Symbol)
37
37
  raise ArgumentError unless @@Allowed_symbols.include?(value)
@@ -0,0 +1,21 @@
1
+ module RSokoban::UI
2
+
3
+ # I am an image of the game who knows how to display itself on an array
4
+ # of TkLabel items.
5
+ # @since 0.73
6
+ class TkBox
7
+
8
+ # @param [String] file path of image file
9
+ # @param [Array<Array<TkLabel>>] output an array to display myself
10
+ def initialize file, output
11
+ @box = TkPhotoImage.new('file' => file, 'height' => 0, 'width' => 0)
12
+ @output = output
13
+ end
14
+
15
+ # Display myself at x,y coordinate
16
+ def display_at x, y
17
+ @output[y][x].configure('image' => @box)
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,240 @@
1
+ module RSokoban::UI
2
+
3
+ # As dialog box, I allow the user to choose a specific level number.
4
+ class TkLevelDialog < TkToplevel
5
+
6
+ # Create and show the dialog
7
+ # @param [TkRoot|TkToplevel] root the Tk widget I belong to
8
+ # @param [String] title my window title
9
+ def initialize(root, title)
10
+ super(root)
11
+ title(title)
12
+ minsize(200, 100)
13
+ @state = :cancel
14
+ grab
15
+
16
+ @frame_north = TkFrame.new(self) do
17
+ grid(:row => 0, :column => 0, :columnspan => 2, :sticky => :we)
18
+ padx 10
19
+ pady 10
20
+ end
21
+
22
+ $spinval = TkVariable.new
23
+ @spin = TkSpinbox.new(@frame_north) do
24
+ textvariable($spinval)
25
+ width 3
26
+ grid(:row => 0, :column => 0)
27
+ end
28
+ @spin.to(999)
29
+ @spin.from(1)
30
+ @spin.focus
31
+ @spin.bind 'Key-Return', proc{ ok_on_clic }
32
+
33
+ @ok = TkButton.new(self) do
34
+ text 'OK'
35
+ grid(:row => 1, :column => 0)
36
+ default :active
37
+ end
38
+ @ok.command { ok_on_clic }
39
+
40
+ @cancel = TkButton.new(self) do
41
+ text 'Cancel'
42
+ grid(:row => 1, :column => 1)
43
+ end
44
+ @cancel.command { cancel_on_clic }
45
+
46
+ wait_destroy
47
+ end
48
+
49
+ # @return true if user clicked the OK button
50
+ def ok?
51
+ @state == :ok
52
+ end
53
+
54
+ # @return [Fixnum] level number
55
+ def value
56
+ $spinval.to_i
57
+ end
58
+
59
+ private
60
+
61
+ def ok_on_clic
62
+ @state = :ok
63
+ destroy
64
+ end
65
+
66
+ def cancel_on_clic
67
+ @state = :cancel
68
+ destroy
69
+ end
70
+ end
71
+
72
+ # As dialog box, I allow the user to choose a set name.
73
+ class SetDialog < TkToplevel
74
+ include RSokoban
75
+ # Create and show the dialog
76
+ # @param [TkRoot|TkToplevel] root the Tk widget I belong to
77
+ # @param [String] title my window title
78
+ def initialize(root, title)
79
+ super(root)
80
+ title(title)
81
+ width(300)
82
+ height(400)
83
+ @state = 'CANCEL'
84
+ grab
85
+ self['resizable'] = false, false
86
+
87
+ @xsb = get_xsb
88
+ $listval = TkVariable.new(@xsb)
89
+ @value = @xsb[0]
90
+
91
+ # A frame for the listbox
92
+ @frame_north = TkFrame.new(self) do
93
+ grid(:row => 0, :column => 0, :columnspan => 2, :sticky => :we)
94
+ padx 10
95
+ pady 10
96
+ end
97
+
98
+ @list = TkListbox.new(@frame_north) do
99
+ width 40
100
+ height 8
101
+ listvariable $listval
102
+ grid(:row => 0, :column => 0, :sticky => :we)
103
+ end
104
+
105
+ @list.bind '<ListboxSelect>', proc{ show_description }
106
+ @list.bind 'Double-1', proc{ ok_on_clic }
107
+ @list.bind 'Return', proc{ ok_on_clic }
108
+
109
+
110
+ scroll = TkScrollbar.new(@frame_north) do
111
+ orient 'vertical'
112
+ grid(:row => 0, :column => 1, :sticky => :ns)
113
+ end
114
+
115
+ @list.yscrollcommand(proc { |*args| scroll.set(*args) })
116
+ scroll.command(proc { |*args| @list.yview(*args) })
117
+
118
+ # A frame for the set description
119
+ @frame_desc = TkFrame.new(self) do
120
+ grid(:row => 1, :column => 0, :columnspan => 2, :sticky => :we)
121
+ padx 10
122
+ pady 10
123
+ end
124
+
125
+ @desc = TkText.new(@frame_desc) do
126
+ borderwidth 1
127
+ font TkFont.new('times 12')
128
+ width 40
129
+ height 10
130
+ wrap :word
131
+ grid(:row => 0, :column => 0)
132
+ end
133
+
134
+ scroll2 = TkScrollbar.new(@frame_desc) do
135
+ orient 'vertical'
136
+ grid(:row => 0, :column => 1, :sticky => :ns)
137
+ end
138
+
139
+ @desc.yscrollcommand(proc { |*args| scroll2.set(*args) })
140
+ scroll2.command(proc { |*args| @desc.yview(*args) })
141
+
142
+ # The buttons
143
+ @ok = TkButton.new(self) do
144
+ text 'OK'
145
+ grid(:row => 2, :column => 0)
146
+ default :active
147
+ end
148
+ @ok.command { ok_on_clic }
149
+
150
+ @cancel = TkButton.new(self) do
151
+ text 'Cancel'
152
+ grid(:row => 2, :column => 1)
153
+ end
154
+ @cancel.command { cancel_on_clic }
155
+
156
+ @list.focus
157
+ wait_destroy
158
+ end
159
+
160
+ # @return true if user clicked the OK button
161
+ def ok?
162
+ @state == 'OK'
163
+ end
164
+
165
+ # @return [String] the name of the set
166
+ def value
167
+ @value
168
+ end
169
+
170
+ private
171
+
172
+ def ok_on_clic
173
+ @state = 'OK'
174
+ idx = @list.curselection
175
+ unless idx.empty?
176
+ @value = @xsb[idx[0]]
177
+ end
178
+ destroy
179
+ end
180
+
181
+ def cancel_on_clic
182
+ @state = 'CANCEL'
183
+ destroy
184
+ end
185
+
186
+ def get_xsb
187
+ current = Dir.pwd
188
+ Dir.chdir $RSOKOBAN_DATA_PATH
189
+ ret = Dir.glob '*.xsb'
190
+ Dir.chdir current
191
+ ret
192
+ end
193
+
194
+ def show_description
195
+ idx = @list.curselection
196
+ ll = LevelLoader.new @xsb[idx[0]]
197
+ @desc.delete '1.0', :end
198
+ @desc.insert :end, ll.file_description
199
+ end
200
+ end
201
+
202
+ # As dialog box, I display some help.
203
+ class HelpDialog < TkToplevel
204
+ # Create and show the dialog
205
+ # @param [TkRoot|TkToplevel] root the Tk widget I belong to
206
+ # @param [String] title my window title
207
+ def initialize(root, title)
208
+ super(root)
209
+ title(title)
210
+ minsize(200, 100)
211
+
212
+ text = TkText.new(self) do
213
+ borderwidth 1
214
+ font TkFont.new('times 12 bold')
215
+ grid('row' => 0, 'column' => 0)
216
+ end
217
+
218
+ help=<<EOS
219
+ Welcome to RSokoban !
220
+
221
+ Goal of Sokoban game is to place each crate on a storage location.
222
+ Move the man using the arrow keys.
223
+ For a more comprehensive help, please visit the wiki at https://github.com/lkdjiin/RSokoban/wiki.
224
+ EOS
225
+
226
+ text.insert 'end', help
227
+
228
+ @ok = TkButton.new(self) do
229
+ text 'OK'
230
+ grid('row'=>1, 'column'=>0)
231
+ end
232
+ @ok.command { ok_on_clic }
233
+ end
234
+
235
+ def ok_on_clic
236
+ destroy
237
+ end
238
+ end
239
+
240
+ end