sokoban 0.0.16 → 0.0.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sokoban.rb +32 -23
- data/lib/sokoban/version.rb +1 -1
- data/spec/lib/sokoban_spec.rb +25 -35
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a921867ffc4091581ea2ba07a96d0fd98ede029
|
4
|
+
data.tar.gz: 7a66174afaff10b372deedbe8dc342348ee0bf57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52e6766c2d227cc6aa5beb2afa5c83b22df5156af6413421b2c4bcc5edc3d1f2d8512965fb2f042328e738d085e4e47e3b222355a013b848ece8f5c287140a0d
|
7
|
+
data.tar.gz: c9aaea1824af81935a5dedd4d45855c0787c0d98e2355cd88340b3bed1c15cf6e2b31ace35c700b70449b0878c69472e208552327d1d113513024f00f4de8ef0
|
data/lib/sokoban.rb
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
# Copyright 2004 Dennis Ranke <dennis.ranke at epost.de>
|
8
8
|
# Copyright 2013 Boohbah <boohbah at gmail.com>
|
9
9
|
|
10
|
-
require File.expand_path(
|
10
|
+
require File.expand_path('../../lib/sokoban/version.rb', __FILE__)
|
11
11
|
require 'io/console'
|
12
12
|
require 'optparse'
|
13
13
|
require 'logger'
|
@@ -18,7 +18,7 @@ module Sokoban
|
|
18
18
|
BASENAME = File.basename($0, '.rb')
|
19
19
|
SAV_FILE = "#{HOME}/sokoban.save.gz"
|
20
20
|
LOG_FILE = "#{HOME}/sokoban-#{Time.now.to_i}.log"
|
21
|
-
LVL_FILE = File.expand_path(
|
21
|
+
LVL_FILE = File.expand_path('../../sokoban_levels.txt', __FILE__)
|
22
22
|
|
23
23
|
LEVELS = File.open(LVL_FILE).readlines.map do |l|
|
24
24
|
l.chomp.ljust(19)
|
@@ -33,11 +33,16 @@ module Sokoban
|
|
33
33
|
D: { x: 1, y: 0 } # 4
|
34
34
|
}
|
35
35
|
|
36
|
+
# Global-ish variables to ease testing
|
37
|
+
$batch, $hax = false, false
|
38
|
+
|
39
|
+
# Game class separates logger and optparse from internal
|
40
|
+
# Level logic and handles level changes and win condition
|
36
41
|
class Game
|
37
42
|
def initialize
|
38
43
|
@level = Level.new
|
39
44
|
parse_opts
|
40
|
-
#
|
45
|
+
# --level and --resume may change lnum
|
41
46
|
# set @lnum after parse_opts
|
42
47
|
@lnum = @level.lnum
|
43
48
|
play
|
@@ -151,16 +156,21 @@ module Sokoban
|
|
151
156
|
@moves.size
|
152
157
|
end
|
153
158
|
|
159
|
+
# The public interface to initialize()
|
154
160
|
def restart(lnum)
|
155
161
|
initialize(lnum)
|
156
162
|
end
|
157
163
|
|
164
|
+
def parse_move_string(string)
|
165
|
+
string.each_char {|c| parse_move(c) }
|
166
|
+
end
|
167
|
+
|
158
168
|
def play
|
159
169
|
while free_boxes > 0
|
160
170
|
print self
|
161
171
|
if $batch
|
162
172
|
# Input a string terminated by newline
|
163
|
-
gets.strip
|
173
|
+
parse_move_string(gets.strip)
|
164
174
|
else
|
165
175
|
# Handle single key press
|
166
176
|
parse_move(STDIN.getch)
|
@@ -174,7 +184,7 @@ module Sokoban
|
|
174
184
|
unless @lnum == 50
|
175
185
|
print "Congratulations, on to level #{@lnum + 1}. "
|
176
186
|
end
|
177
|
-
STDIN.getch
|
187
|
+
STDIN.getch
|
178
188
|
end
|
179
189
|
|
180
190
|
def free_boxes
|
@@ -195,12 +205,10 @@ module Sokoban
|
|
195
205
|
|
196
206
|
def load_saved_game
|
197
207
|
file = Zlib::GzipReader.open(SAV_FILE) {|f| f.read }
|
198
|
-
# Gzip becomes space-efficient when @moves is ~100 or more
|
199
208
|
file = file.unpack('C' * file.size)
|
200
|
-
# Initialize
|
201
|
-
@lnum = file.shift
|
202
|
-
@
|
203
|
-
@state = [@level.dup]
|
209
|
+
# Initialize Level with @lnum from file
|
210
|
+
@lnum = file.shift
|
211
|
+
initialize(@lnum)
|
204
212
|
# Rebuild @moves
|
205
213
|
moves = []
|
206
214
|
file.map {|c| moves << unpack_nibs(c) }
|
@@ -208,13 +216,13 @@ module Sokoban
|
|
208
216
|
end
|
209
217
|
|
210
218
|
def parse_move(input)
|
211
|
-
case input
|
219
|
+
case input.downcase
|
212
220
|
when 'b'; $batch = ! $batch
|
213
221
|
when 'w', 'k', '8'; move(:W) #UP)
|
214
222
|
when 'a', 'h', '4'; move(:A) #LEFT)
|
215
223
|
when 's', 'j', '2'; move(:S) #DOWN)
|
216
224
|
when 'd', 'l', '6'; move(:D) #RIGHT)
|
217
|
-
when 'r';
|
225
|
+
when 'r'; initialize(@lnum)
|
218
226
|
when 'u'; undo
|
219
227
|
when 'q'; quit
|
220
228
|
when "\u0003"; puts; exit # ^C - Quit without save
|
@@ -223,28 +231,29 @@ module Sokoban
|
|
223
231
|
|
224
232
|
private
|
225
233
|
|
226
|
-
|
227
|
-
|
228
|
-
a, b = ("%2X" % c).split(//).map(&:to_i)
|
229
|
-
b.zero? ? a : [a, b]
|
234
|
+
def clear_screen
|
235
|
+
30.times { puts }
|
230
236
|
end
|
231
237
|
|
232
238
|
def pack_nibs(a, b)
|
233
239
|
b.nil? ? (a << 4) | 0 : (a << 4) | b
|
234
240
|
end
|
235
241
|
|
242
|
+
def unpack_nibs(c)
|
243
|
+
a, b = ("%2X" % c).split(//).map(&:to_i)
|
244
|
+
b.zero? ? a : [a, b]
|
245
|
+
end
|
246
|
+
|
236
247
|
def save_game
|
237
248
|
file = [@lnum]
|
249
|
+
# @moves are 1, 2, 3, or 4 to enable easy bit packing
|
238
250
|
@moves.each_slice(2) {|s| file << pack_nibs(s[0], s[1]) }
|
239
251
|
file = file.pack('C' * file.size)
|
252
|
+
# Gzip becomes space-efficient when @moves is ~99
|
240
253
|
Zlib::GzipWriter.open(SAV_FILE) {|f| f.write(file) }
|
241
254
|
# $ zcat sokoban.save.gz | hexdump
|
242
255
|
end
|
243
256
|
|
244
|
-
def clear_screen
|
245
|
-
30.times { puts }
|
246
|
-
end
|
247
|
-
|
248
257
|
def undo
|
249
258
|
if @moves.size > 0
|
250
259
|
@moves.pop
|
@@ -255,9 +264,9 @@ module Sokoban
|
|
255
264
|
end
|
256
265
|
|
257
266
|
def box_moved?
|
258
|
-
|
259
|
-
if
|
260
|
-
return true if @state.last[i] !=
|
267
|
+
@level.split(//).each_with_index do |char, i|
|
268
|
+
if char =~ /o|\*/
|
269
|
+
return true if @state.last[i] != char
|
261
270
|
end
|
262
271
|
end
|
263
272
|
return false
|
data/lib/sokoban/version.rb
CHANGED
data/spec/lib/sokoban_spec.rb
CHANGED
@@ -35,99 +35,89 @@ describe Level do
|
|
35
35
|
expect { Level.new(54) }.to raise_error(RangeError)
|
36
36
|
end
|
37
37
|
|
38
|
-
def input_moves(level)
|
39
|
-
INPUT.each_char {|c| level.parse_move(c.downcase) }
|
40
|
-
end
|
41
|
-
|
42
38
|
it "accepts valid moves" do
|
43
39
|
level = Level.new
|
44
|
-
|
40
|
+
level.parse_move_string(INPUT)
|
45
41
|
expect(level.moves).to eq(INPUT.size)
|
46
42
|
end
|
47
43
|
|
48
44
|
it "ignores invalid moves" do
|
49
45
|
level = Level.new
|
50
|
-
|
51
|
-
level.parse_move(
|
46
|
+
level.parse_move_string(INPUT)
|
47
|
+
level.parse_move("F") # shouldn"t increment moves
|
52
48
|
expect(level.moves).to eq(INPUT.size)
|
53
49
|
end
|
54
50
|
|
55
51
|
it "restarts" do
|
56
52
|
level = Level.new
|
57
|
-
|
53
|
+
level.parse_move_string(INPUT)
|
58
54
|
expect(level.free_boxes).to eq(1)
|
59
|
-
level.parse_move(
|
55
|
+
level.parse_move("R") # restart
|
60
56
|
expect(level.free_boxes).to eq(6)
|
61
57
|
end
|
62
58
|
|
63
59
|
it "plays level 1" do
|
64
60
|
level = Level.new
|
65
|
-
|
66
|
-
level.parse_move(
|
61
|
+
level.parse_move_string(INPUT)
|
62
|
+
level.parse_move("D") # right
|
67
63
|
expect(level.free_boxes).to eq(0) # Victory!
|
68
64
|
end
|
69
65
|
|
70
66
|
it "increments pushes on box move" do
|
71
67
|
level = Level.new
|
72
|
-
|
68
|
+
level.parse_move_string(INPUT)
|
73
69
|
expect(level.pushes).to eq(96)
|
74
|
-
level.parse_move(
|
70
|
+
level.parse_move("D") # right
|
75
71
|
expect(level.pushes).to eq(97)
|
76
72
|
end
|
77
73
|
|
78
74
|
it "decrements pushes on box move undo" do
|
79
75
|
level = Level.new
|
80
|
-
|
81
|
-
level.parse_move(
|
76
|
+
level.parse_move_string(INPUT)
|
77
|
+
level.parse_move("D") # right
|
82
78
|
expect(level.pushes).to eq(97)
|
83
|
-
level.parse_move(
|
79
|
+
level.parse_move("U") # undo
|
84
80
|
expect(level.pushes).to eq(96)
|
85
81
|
end
|
86
82
|
|
87
83
|
it "ignores moves into walls" do
|
88
84
|
level = Level.new
|
89
|
-
|
90
|
-
level.parse_move(
|
85
|
+
level.parse_move_string(INPUT)
|
86
|
+
level.parse_move("A") # left
|
91
87
|
expect(level.moves).to eq(INPUT.size + 1)
|
92
88
|
# Try to move into wall
|
93
|
-
level.parse_move(
|
89
|
+
level.parse_move("W") # up
|
94
90
|
expect(level.moves).to eq(INPUT.size + 1)
|
95
91
|
end
|
96
92
|
|
97
93
|
it "ignores moves into non-free boxes" do
|
98
94
|
level = Level.new
|
99
|
-
|
100
|
-
level.
|
101
|
-
level.parse_move('d') # right
|
95
|
+
level.parse_move_string(INPUT)
|
96
|
+
level.parse_move_string("SD")
|
102
97
|
expect(level.moves).to eq(INPUT.size + 2)
|
103
98
|
# Try to move into non-free box
|
104
|
-
level.parse_move(
|
99
|
+
level.parse_move("D") # right
|
105
100
|
expect(level.moves).to eq(INPUT.size + 2)
|
106
101
|
end
|
107
102
|
|
108
103
|
it "ignores pushes into walls" do
|
109
104
|
level = Level.new
|
110
|
-
|
111
|
-
level.
|
112
|
-
level.parse_move('d') # right
|
113
|
-
level.parse_move('w') # up
|
105
|
+
level.parse_move_string(INPUT)
|
106
|
+
level.parse_move_string("SDW") # down, right, up
|
114
107
|
expect(level.pushes).to eq(97)
|
115
108
|
# Try to push box into wall
|
116
|
-
level.parse_move(
|
109
|
+
level.parse_move("W") # up
|
117
110
|
expect(level.pushes).to eq(97)
|
118
111
|
end
|
119
112
|
|
120
113
|
it "ignores pushes into non-free boxes" do
|
121
114
|
level = Level.new
|
122
|
-
|
123
|
-
level.
|
124
|
-
level.parse_move('d') # right
|
125
|
-
level.parse_move('s') # down
|
115
|
+
level.parse_move_string(INPUT)
|
116
|
+
level.parse_move_string("WDS") # up, right, down
|
126
117
|
expect(level.pushes).to eq(97)
|
127
|
-
level.
|
128
|
-
level.parse_move('s') # down
|
118
|
+
level.parse_move_string("AS") # left, down
|
129
119
|
# Try to push box into non-free box
|
130
|
-
level.parse_move(
|
120
|
+
level.parse_move("D") # right
|
131
121
|
expect(level.pushes).to eq(97)
|
132
122
|
end
|
133
123
|
end
|