RSokoban 0.73 → 0.74

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.
@@ -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