pwn 0.5.749 → 0.5.751
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/documentation/Blockchain.md +107 -9
- data/documentation/pwn-ai-Agent.md +310 -4
- data/documentation/pwn_silent_help_learn_demo.gif +0 -0
- data/etc/default_skills/pwn/banner/SKILL.md +7 -3
- data/etc/default_skills/pwn/blockchain/btc/SKILL.md +8 -5
- data/etc/default_skills/pwn/blockchain/eth/SKILL.md +12 -3
- data/etc/default_skills/pwn/blockchain/eth/references/urls.md +1 -1
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +5 -0
- data/etc/default_skills/pwn/plugins/repl/ai/SKILL.md +4 -0
- data/etc/default_skills/pwn/plugins/repl/ai/references/urls.md +5 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console/SKILL.md +48 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console_commands/SKILL.md +47 -0
- data/etc/default_skills/pwn/plugins/repl/ai_console_usage/SKILL.md +48 -0
- data/etc/default_skills/pwn/plugins/repl/ai_swarm/SKILL.md +46 -0
- data/lib/pwn/ai/agent/loop.rb +287 -18
- data/lib/pwn/ai/agent/swarm.rb +28 -4
- data/lib/pwn/ai/open_ai.rb +4 -2
- data/lib/pwn/banner.rb +743 -2
- data/lib/pwn/blockchain/btc.rb +494 -196
- data/lib/pwn/blockchain/eth.rb +440 -81
- data/lib/pwn/blockchain.rb +6 -0
- data/lib/pwn/config.rb +22 -0
- data/lib/pwn/migrate.rb +9 -1
- data/lib/pwn/plugins/repl/ai.rb +182 -17
- data/lib/pwn/plugins/repl/ai_console.rb +1581 -0
- data/lib/pwn/plugins/repl/ai_console_commands.rb +282 -0
- data/lib/pwn/plugins/repl/ai_console_usage.rb +144 -0
- data/lib/pwn/plugins/repl/ai_swarm.rb +375 -0
- data/lib/pwn/plugins/repl.rb +25 -3
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +258 -1
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +3 -2
- data/spec/lib/pwn/ai/open_ai_spec.rb +5 -0
- data/spec/lib/pwn/banner_spec.rb +643 -0
- data/spec/lib/pwn/blockchain/btc_spec.rb +372 -1
- data/spec/lib/pwn/blockchain/eth_spec.rb +205 -0
- data/spec/lib/pwn/blockchain_spec.rb +6 -0
- data/spec/lib/pwn/config_spec.rb +10 -0
- data/spec/lib/pwn/migrate_spec.rb +40 -3
- data/spec/lib/pwn/plugins/repl/ai_console_commands_spec.rb +65 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +1434 -0
- data/spec/lib/pwn/plugins/repl/ai_console_usage_spec.rb +29 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +127 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +5 -3
- data/spec/lib/pwn/plugins/repl_spec.rb +167 -1
- data/third_party/pwn_rdoc.jsonl +229 -10
- metadata +18 -6
- data/etc/default_skills/pwn/blockchain/btc/references/urls.md +0 -3
data/spec/lib/pwn/banner_spec.rb
CHANGED
|
@@ -3,6 +3,649 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::Banner do
|
|
6
|
+
def block_colors(rows)
|
|
7
|
+
rows.flat_map do |row|
|
|
8
|
+
[0, 2].map do |bit|
|
|
9
|
+
row.chars.map do |pixel|
|
|
10
|
+
glyph, foreground, background = described_class::MINI_PIXELS.fetch(pixel)
|
|
11
|
+
mask = ' ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█'.index(glyph)
|
|
12
|
+
mask[bit] == 1 ? foreground : background || :black
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'packs every rotation losslessly beside different colors, holes and white flashes' do
|
|
19
|
+
described_class::MINI_SHAPES.each_with_index do |shape, type|
|
|
20
|
+
described_class.send(:mini_rotations, shape: shape).each do |rotation|
|
|
21
|
+
[0, 1].each do |offset|
|
|
22
|
+
source = Array.new(8) { 'cmywgrb '.dup }
|
|
23
|
+
rotation.each { |x, y| source[y + offset][x + 2] = 'cymbrgg'[type] }
|
|
24
|
+
packed = described_class.send(:mini_block_board, board: source)
|
|
25
|
+
expected = source.map { |row| row.chars.map { |pixel| pixel == ' ' ? :black : described_class::MINI_PIXELS.fetch(pixel)[1] } }
|
|
26
|
+
expect(block_colors(packed)).to eq(expected)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'keeps every active tetromino color beside the stack throughout its fall and lock' do
|
|
33
|
+
rotations = Hash.new { |hash, key| hash[key] = [] }
|
|
34
|
+
[5, 8, 16, 20].product([1, 73, 74]).each do |size, seed|
|
|
35
|
+
args = { name: :falling_blocks, width: size, height: size, seed: seed }
|
|
36
|
+
frames = described_class.send(:mini_replay, **args)
|
|
37
|
+
frames.each_with_index do |frame, index|
|
|
38
|
+
next unless frame[:state][:active]
|
|
39
|
+
|
|
40
|
+
points = frame[:state][:active]
|
|
41
|
+
type = frame[:state][:type]
|
|
42
|
+
cells = described_class.mini_cells(**args, frame: index)
|
|
43
|
+
color = %i[cyan yellow magenta blue red green green][type]
|
|
44
|
+
expected = frame[:state][:stack].map { |row| row.chars.map { |pixel| pixel == ' ' ? :black : described_class::MINI_PIXELS.fetch(pixel)[1] } }
|
|
45
|
+
points.each { |x, y| expected[y][x] = color }
|
|
46
|
+
expect(block_colors(frame[:rows].drop(1))).to eq(expected)
|
|
47
|
+
points.each do |x, y|
|
|
48
|
+
cell = cells[1 + (y / 2)][x]
|
|
49
|
+
mask = ' ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█'.index(cell[:glyph])
|
|
50
|
+
actual = mask[(y % 2) * 2] == 1 ? cell[:foreground] : cell[:background]
|
|
51
|
+
expect(actual).to eq(color)
|
|
52
|
+
end
|
|
53
|
+
next unless frames[index + 1][:event] == :lock
|
|
54
|
+
|
|
55
|
+
expect(frames[index + 1][:rows]).to eq(frame[:rows])
|
|
56
|
+
expect(described_class.mini_cells(**args, frame: index + 1)).to eq(cells)
|
|
57
|
+
rotations[type] << points.map { |x, y| [x - points.map(&:first).min, y - points.map(&:last).min] }.sort
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
described_class::MINI_SHAPES.each_with_index do |shape, type|
|
|
61
|
+
expected = described_class.send(:mini_rotations, shape: shape).map(&:sort).uniq
|
|
62
|
+
expect(rotations[type].uniq).to match_array(expected)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'changes only completed rows to white on clear and preserves survivor colors on collapse' do
|
|
67
|
+
frames = described_class.send(:mini_replay, name: :falling_blocks, width: 8, height: 8, seed: 73)
|
|
68
|
+
clears = 0
|
|
69
|
+
frames.each_cons(2) do |before, after|
|
|
70
|
+
next unless after[:event] == :clear && before[:event] == :lock
|
|
71
|
+
|
|
72
|
+
expected = block_colors(before[:rows].drop(1))
|
|
73
|
+
after[:state][:lines].each { |y| expected[y] = Array.new(8, :white) }
|
|
74
|
+
expect(block_colors(after[:rows].drop(1))).to eq(expected)
|
|
75
|
+
clears += 1
|
|
76
|
+
end
|
|
77
|
+
expect(clears).to be_positive
|
|
78
|
+
frames.each_cons(2) do |before, after|
|
|
79
|
+
next unless before[:event] == :clear && after[:event] == :collapse
|
|
80
|
+
|
|
81
|
+
lines = before[:state][:lines]
|
|
82
|
+
survivors = block_colors(before[:rows].drop(1)).each_with_index.filter_map { |row, y| row unless lines.include?(y) }
|
|
83
|
+
expect(block_colors(after[:rows].drop(1))).to eq(Array.new(lines.length) { Array.new(8, :black) } + survivors)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'lands blocks on both bottom quadrants and spans the full board width' do
|
|
88
|
+
[5, 8, 16, 20].each do |size|
|
|
89
|
+
frames = described_class.send(:mini_replay, name: :falling_blocks, width: size, height: size, seed: 73)
|
|
90
|
+
locks = frames.select { |frame| frame[:event] == :lock }
|
|
91
|
+
expect(locks.first[:rows].last.chars.any? { |pixel| pixel != ' ' && ((pixel.ord - 256) % 16) & 12 != 0 }).to be(true)
|
|
92
|
+
[0, size - 1].each do |column|
|
|
93
|
+
expect(locks.any? { |frame| frame[:rows].drop(1).any? { |row| row[column] != ' ' } }).to be(true)
|
|
94
|
+
end
|
|
95
|
+
cells = described_class.mini_cells(name: :falling_blocks, width: size, height: size, frame: 100)
|
|
96
|
+
expect(cells.length).to eq(size)
|
|
97
|
+
expect(cells.map(&:length)).to all(eq(size))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'packs blocks into two quadrants and snake segments into one without losing ink' do
|
|
102
|
+
%i[falling_blocks snake].product((5..16).to_a).each do |name, size|
|
|
103
|
+
frames = described_class.send(:mini_replay, name: name, width: size, height: size, seed: 73)
|
|
104
|
+
frames.each do |frame|
|
|
105
|
+
pixels = frame[:rows].drop(1).join.chars.reject { |pixel| pixel == ' ' }
|
|
106
|
+
expect(pixels).to all(satisfy { |pixel| pixel.ord >= 256 })
|
|
107
|
+
area = pixels.sum { |pixel| ((pixel.ord - 256) % 16).digits(2).sum }
|
|
108
|
+
state = frame[:state]
|
|
109
|
+
expected = if state[:body]
|
|
110
|
+
state[:body].length + (state[:food] ? 1 : 0)
|
|
111
|
+
elsif state[:active]
|
|
112
|
+
(state[:stack].sum { |row| row.count('^ ') } + 4) * 2
|
|
113
|
+
end
|
|
114
|
+
expect(area).to eq(expected) if expected
|
|
115
|
+
expect(frame[:rows].length).to eq(size)
|
|
116
|
+
expect(frame[:rows].map(&:length)).to all(eq(size))
|
|
117
|
+
expect(frame[:rows].first).to eq('PWN'.center(size))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'keeps snake food legible without erasing shared-cell quadrants' do
|
|
123
|
+
# Head, body and food may share a character cell, but never lose occupied
|
|
124
|
+
# quadrants. Food wins its cell's color; yellow head wins over green body.
|
|
125
|
+
rows = described_class.send(:mini_quarter_board, board: ['gyf ', ' ', ' ', ' '], name: 'snake')
|
|
126
|
+
expect(described_class::MINI_PIXELS.fetch(rows[1][1])).to eq(['▀', :yellow])
|
|
127
|
+
expect(described_class::MINI_PIXELS.fetch(rows[1][2])).to eq(['▘', :red])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'draws a tiny connected five-pixel arrowhead rather than a chunky rotating blob' do
|
|
131
|
+
8.times do |direction|
|
|
132
|
+
frames = []
|
|
133
|
+
described_class.send(:mini_asteroids_snapshot, frames: frames, width: 8, height: 7, ship: [4, 3, direction * Math::PI / 4], thrust: false, rocks: [], shots: [], sparks: [])
|
|
134
|
+
points = []
|
|
135
|
+
frames.first[:rows].drop(1).each_with_index do |row, y|
|
|
136
|
+
row.chars.each_with_index do |pixel, x|
|
|
137
|
+
next if pixel == ' '
|
|
138
|
+
|
|
139
|
+
mask = (pixel.ord - 256) % 16
|
|
140
|
+
4.times { |bit| points << [(x * 2) + (bit % 2), (y * 2) + (bit / 2)] if mask[bit] == 1 }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
expect(points.length).to eq(5)
|
|
144
|
+
expect(points.map(&:first).minmax.then { |a, b| b - a }).to eq(2)
|
|
145
|
+
expect(points.map(&:last).minmax.then { |a, b| b - a }).to eq(2)
|
|
146
|
+
connected = [points.shift]
|
|
147
|
+
loop do
|
|
148
|
+
adjacent = points.select { |point| connected.any? { |other| point.zip(other).all? { |a, b| (a - b).abs <= 1 } } }
|
|
149
|
+
break if adjacent.empty?
|
|
150
|
+
|
|
151
|
+
connected.concat(adjacent)
|
|
152
|
+
points -= adjacent
|
|
153
|
+
end
|
|
154
|
+
expect(points).to be_empty
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'offers seeded Asteroids with rotating thrust, wrapped drift, shots and real breakups' do
|
|
159
|
+
expect(described_class.mini_names).to include(:asteroids)
|
|
160
|
+
expect(described_class.mini_frame_seconds(name: :asteroids)).to eq(0.05)
|
|
161
|
+
(5..16).each do |size|
|
|
162
|
+
frames = described_class.send(:mini_replay, name: :asteroids, width: size, height: size, seed: 73)
|
|
163
|
+
states = frames.filter_map { |frame| frame[:state] if frame[:state][:ship] }
|
|
164
|
+
expect(frames.map { |frame| frame[:event] }).to include(:hit, :respawn)
|
|
165
|
+
expect(states.map { |state| state[:thrust] }.uniq).to contain_exactly(true, false)
|
|
166
|
+
expect(states.map { |state| state[:ship][2].round(1) }.uniq.length).to be > 12
|
|
167
|
+
expect(states.map { |state| state[:shots].length }.max).to be_between(1, 2)
|
|
168
|
+
expect(states.map { |state| state[:rocks].length }.max).to be <= [3, size / 4].min + 1
|
|
169
|
+
expect(states.flat_map { |state| state[:rocks].map { |rock| rock[4] } }).to include(0.2, 0.5)
|
|
170
|
+
states.each_cons(2) do |a, b|
|
|
171
|
+
expect((((b[:ship][0] - a[:ship][0] + (size / 2.0)) % size) - (size / 2.0)).abs).to be <= 0.16
|
|
172
|
+
a[:rocks].each do |rock|
|
|
173
|
+
next_rock = b[:rocks].find { |candidate| candidate[5] == rock[5] }
|
|
174
|
+
next unless next_rock
|
|
175
|
+
|
|
176
|
+
expect(next_rock[0]).to be_within(0.000001).of((rock[0] + rock[2]) % size)
|
|
177
|
+
expect(next_rock[1]).to be_within(0.000001).of((rock[1] + rock[3]) % (size - 1))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'keeps rocks small and neutral with sparse quarter-cell shots and thrust' do
|
|
184
|
+
frames = []
|
|
185
|
+
described_class.send(:mini_asteroids_snapshot, frames: frames, width: 8, height: 7, ship: [4, 3, 0], thrust: false,
|
|
186
|
+
rocks: [[1, 1, 0, 0, 0.5, 1]], shots: [], sparks: [])
|
|
187
|
+
rock_pixels = frames.first[:rows].drop(1).flat_map(&:chars).reject { |pixel| pixel == ' ' || (pixel.ord - 256) / 16 == 0 }
|
|
188
|
+
expect(rock_pixels.map { |pixel| described_class::MINI_PIXELS.fetch(pixel)[1] }.uniq).to eq([:white])
|
|
189
|
+
expect(rock_pixels.sum { |pixel| ((pixel.ord - 256) % 16).digits(2).sum }).to eq(4)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'keeps a readable five-pixel ship and sparse subcell occupancy throughout tiny seeded replays' do
|
|
193
|
+
[5, 8, 16].product([1, 73, 74]).each do |size, seed|
|
|
194
|
+
frames = described_class.send(:mini_replay, name: :asteroids, width: size, height: size, seed: seed)
|
|
195
|
+
frames.each do |frame|
|
|
196
|
+
next unless frame[:state][:ship]
|
|
197
|
+
|
|
198
|
+
pixels = frame[:rows].drop(1).flat_map(&:chars).reject { |pixel| pixel == ' ' }
|
|
199
|
+
area = pixels.sum { |pixel| ((pixel.ord - 256) % 16).digits(2).sum }
|
|
200
|
+
ship = pixels.select { |pixel| (pixel.ord - 256) / 16 == 0 }
|
|
201
|
+
expect(ship.sum { |pixel| ((pixel.ord - 256) % 16).digits(2).sum }).to eq(5)
|
|
202
|
+
expect(area).to be <= [20, size * (size - 1)].min
|
|
203
|
+
# A lone quadrant can occupy a cell: measure ink area above, not full
|
|
204
|
+
# character rectangles, while bounding the number of colored cells too.
|
|
205
|
+
expect(pixels.length).to be <= 14
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'spawns rocks clear of the ship instead of starting with overlapping sprites' do
|
|
211
|
+
[5, 8, 16].product([1, 73, 74]).each do |size, seed|
|
|
212
|
+
frames = described_class.send(:mini_replay, name: :asteroids, width: size, height: size, seed: seed)
|
|
213
|
+
state = frames.find { |frame| frame[:state][:ship] }[:state]
|
|
214
|
+
state[:rocks].each do |rock|
|
|
215
|
+
dx = described_class.send(:mini_delta, from: state[:ship][0], to: rock[0], span: size)
|
|
216
|
+
dy = described_class.send(:mini_delta, from: state[:ship][1], to: rock[1], span: size - 1)
|
|
217
|
+
expect((dx * dx) + (dy * dy)).to be > 1.3**2
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'keeps red exhaust visible behind the cyan hull rather than erasing it in shared cells' do
|
|
223
|
+
[5, 8, 16].each do |size|
|
|
224
|
+
frames = described_class.send(:mini_replay, name: :asteroids, width: size, height: size, seed: 73)
|
|
225
|
+
thrust = frames.select { |frame| frame[:state][:thrust] }
|
|
226
|
+
visible = thrust.count { |frame| frame[:rows].drop(1).join.chars.any? { |pixel| (pixel.ord - 256) / 16 == 5 } }
|
|
227
|
+
expect(visible.to_f / thrust.length).to be >= 0.95
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'removes rocks only when an advancing shot actually intersects their wrapped collision radius' do
|
|
232
|
+
[5, 8, 16].each do |size|
|
|
233
|
+
frames = described_class.send(:mini_replay, name: :asteroids, width: size, height: size, seed: 73)
|
|
234
|
+
collisions = 0
|
|
235
|
+
frames.each_cons(2) do |before, after|
|
|
236
|
+
a = before[:state]
|
|
237
|
+
b = after[:state]
|
|
238
|
+
next unless a[:ship] && b[:ship]
|
|
239
|
+
|
|
240
|
+
removed = a[:rocks].reject { |rock| b[:rocks].any? { |other| other[5] == rock[5] } }
|
|
241
|
+
removed.each do |rock|
|
|
242
|
+
expect(after[:event]).to eq(:hit)
|
|
243
|
+
x, y, angle = b[:ship]
|
|
244
|
+
candidates = a[:shots] + [[x, y, Math.cos(angle) * 0.36, Math.sin(angle) * 0.36]]
|
|
245
|
+
expect(candidates.any? do |shot|
|
|
246
|
+
dx = described_class.send(:mini_delta, from: shot[0] + shot[2], to: rock[0] + rock[2], span: size)
|
|
247
|
+
dy = described_class.send(:mini_delta, from: shot[1] + shot[3], to: rock[1] + rock[3], span: size - 1)
|
|
248
|
+
(dx * dx) + (dy * dy) <= rock[4]**2
|
|
249
|
+
end).to be(true)
|
|
250
|
+
collisions += 1
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
expect(collisions).to be > 10
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'uses a faster presentation cadence and subcell, one-cell-high Pong paddles' do
|
|
258
|
+
expect(described_class.mini_frame_seconds(name: :pong)).to eq(0.05)
|
|
259
|
+
expect(described_class.mini_frame_seconds(name: :snake)).to eq(0.1)
|
|
260
|
+
(5..16).each do |size|
|
|
261
|
+
frames = described_class.send(:mini_replay, name: :pong, width: size, height: size, seed: 73)
|
|
262
|
+
states = frames.filter_map { |frame| frame[:state] if frame[:state][:ball] }
|
|
263
|
+
expect(states.map { |state| state[:paddle_height] }.uniq).to eq([1.0])
|
|
264
|
+
frames.each_cons(2) do |a, b|
|
|
265
|
+
next unless a[:state][:ball] && b[:state][:ball]
|
|
266
|
+
|
|
267
|
+
expect(a[:state][:ball].zip(b[:state][:ball]).map { |x, y| (x - y).abs }).to all(be <= 0.51)
|
|
268
|
+
end
|
|
269
|
+
glyphs = (6...180).flat_map { |frame| described_class.mini_cells(name: :pong, width: size, height: size, frame: frame).flatten.map { |cell| cell[:glyph] } }
|
|
270
|
+
expect(glyphs & %w[▘ ▝ ▖ ▗]).not_to be_empty
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'replays extended seeded gameplay instead of repeating every six seconds' do
|
|
275
|
+
expect(described_class::MINI_FRAME_COUNT).to be >= 1800
|
|
276
|
+
described_class.mini_names.each do |name|
|
|
277
|
+
frames = Array.new(240) { |frame| described_class.mini_cells(name: name, frame: frame, seed: 73) }
|
|
278
|
+
expect(frames.uniq.length).to be > 80
|
|
279
|
+
expect(frames[0, 60]).not_to eq(frames[60, 60])
|
|
280
|
+
expect(frames).to eq(Array.new(240) { |frame| described_class.mini_cells(name: name, frame: frame, seed: 73) })
|
|
281
|
+
expect(frames).not_to eq(Array.new(240) { |frame| described_class.mini_cells(name: name, frame: frame, seed: 74) })
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
describe PWN::Banner do
|
|
287
|
+
describe '.mini_cells' do
|
|
288
|
+
it 'returns independent mutable cells and glyph strings even on clear frames' do
|
|
289
|
+
described_class.mini_names.each do |name|
|
|
290
|
+
[0, 26, 58].each do |frame|
|
|
291
|
+
args = { name: name, frame: frame }
|
|
292
|
+
original = described_class.mini_cells(**args)
|
|
293
|
+
changed = described_class.mini_cells(**args)
|
|
294
|
+
expect(changed.flatten.map(&:object_id).uniq.length).to eq(256)
|
|
295
|
+
expect(changed.flatten.map { |cell| cell[:glyph].object_id }.uniq.length).to eq(256)
|
|
296
|
+
changed.flatten.each do |cell|
|
|
297
|
+
cell[:glyph].replace('!')
|
|
298
|
+
cell[:foreground] = :red
|
|
299
|
+
end
|
|
300
|
+
expect(described_class.mini_cells(**args)).to eq(original)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'keeps every frame bounded, single-column, named-color, deterministic and side-effect free' do
|
|
306
|
+
expect(Random).not_to receive(:rand)
|
|
307
|
+
expect(Time).not_to receive(:now)
|
|
308
|
+
expect(Process).not_to receive(:clock_gettime)
|
|
309
|
+
expect(Thread).not_to receive(:new)
|
|
310
|
+
expect(described_class).not_to receive(:sleep)
|
|
311
|
+
expect do
|
|
312
|
+
described_class.mini_names.each do |name|
|
|
313
|
+
(5..16).each do |size|
|
|
314
|
+
args = { name: name, width: size, height: size }
|
|
315
|
+
frames = Array.new(60) { |frame| described_class.mini_cells(**args, frame: frame) }
|
|
316
|
+
expect(frames.uniq.length).to be >= 4
|
|
317
|
+
expect(frames.map(&:length)).to all(eq(size))
|
|
318
|
+
expect(frames.flatten(1).map(&:length)).to all(eq(size))
|
|
319
|
+
frames.flatten(2).each do |cell|
|
|
320
|
+
expect(cell.keys).to contain_exactly(:glyph, :foreground, :background)
|
|
321
|
+
expect(cell[:glyph]).to match(/\A[ PWN█▀▄▌▐░▘▝▖▗▞▛▚▜▙▟]\z/)
|
|
322
|
+
expect(%i[black red green yellow blue magenta cyan white]).to include(cell[:foreground], cell[:background])
|
|
323
|
+
end
|
|
324
|
+
expect(described_class.mini_cells(**args, frame: described_class::MINI_FRAME_COUNT)).to eq(frames.first)
|
|
325
|
+
expect(described_class.mini_cells(**args, frame: -1)).to eq(described_class.mini_cells(**args, frame: described_class::MINI_FRAME_COUNT - 1))
|
|
326
|
+
expect(described_class.mini_cells(**args, name: name.to_s, frame: 23)).to eq(frames[23])
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end.to output('').to_stdout.and output('').to_stderr
|
|
330
|
+
expect(described_class.mini_cells(width: 0)).to eq([])
|
|
331
|
+
expect(described_class.mini_cells(height: -1)).to eq([])
|
|
332
|
+
expect(described_class.mini_cells(name: :snake, width: 100, height: 100)).to eq(described_class.mini_cells(name: :snake))
|
|
333
|
+
expect(described_class.mini_cells(width: 100, height: 100)).to eq(described_class.mini_cells(width: 64, height: 64))
|
|
334
|
+
expect(described_class.mini_cells(width: '3', height: '2').map { |row| row.map { |cell| cell[:glyph] }.join }).to eq(['PWN', ' '])
|
|
335
|
+
expect { described_class.mini_cells(name: :unknown) }.to raise_error(ArgumentError, /Unknown mini-banner/)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def replay(name, size, seed = 73)
|
|
339
|
+
described_class.send(:mini_replay, name: name, width: size, height: size, seed: seed)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def logical_ink(frame, full: false)
|
|
343
|
+
width = frame[:rows].first.length
|
|
344
|
+
height = frame[:rows].length - 1
|
|
345
|
+
height *= 2 if full
|
|
346
|
+
Array.new(height) do |y|
|
|
347
|
+
Array.new(width) do |x|
|
|
348
|
+
px = full ? x * 2 : x + (width / 2)
|
|
349
|
+
py = y + (full ? 0 : height / 2)
|
|
350
|
+
pixel = frame[:rows][1 + (py / 2)][px / 2]
|
|
351
|
+
mask = pixel == ' ' ? 0 : (pixel.ord - 256) % 16
|
|
352
|
+
mask[(py % 2 * 2) + (px % 2)] == 1 ? '*' : ' '
|
|
353
|
+
end.join
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it 'rejects unreachable placements rather than ending a game on a blocked rotation' do
|
|
358
|
+
board = [' ', 'r ', 'r ', 'rr ']
|
|
359
|
+
shape = described_class::MINI_SHAPES.first
|
|
360
|
+
landing = described_class.send(:mini_landing, board: board, shape: shape, rng: Random.new(73))
|
|
361
|
+
rotations = described_class.send(:mini_rotations, shape: shape)
|
|
362
|
+
expect(landing).not_to be_nil
|
|
363
|
+
rotations.take(landing[:rotation] + 1).each do |piece|
|
|
364
|
+
expect(described_class.send(:mini_fits?, board: board, shape: piece, x: 0, y: 0)).to be(true)
|
|
365
|
+
end
|
|
366
|
+
(0..landing[:x]).each do |x|
|
|
367
|
+
expect(described_class.send(:mini_fits?, board: board, shape: landing[:shape], x: x, y: 0)).to be(true)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it 'stacks all seven four-cell tetrominoes with legal gravity, rotations and line collapse' do
|
|
372
|
+
(5..16).each do |size|
|
|
373
|
+
frames = replay(:falling_blocks, size)
|
|
374
|
+
active = frames.select { |frame| frame[:state][:active] }
|
|
375
|
+
expect(active.map { |frame| frame[:state][:type] }.uniq.length).to eq(7)
|
|
376
|
+
expect(frames.map { |frame| frame[:event] }).to include(:lock, :clear, :collapse)
|
|
377
|
+
active.each do |frame|
|
|
378
|
+
points, stack = frame[:state].values_at(:active, :stack)
|
|
379
|
+
expect(points.uniq.length).to eq(4)
|
|
380
|
+
points.each do |x, y|
|
|
381
|
+
expect(x).to be_between(0, size - 1)
|
|
382
|
+
expect(y).to be_between(0, ((size - 1) * 2) - 1)
|
|
383
|
+
expect(stack[y][x]).to eq(' ')
|
|
384
|
+
end
|
|
385
|
+
expected = stack.map { |row| row.gsub(/[^ ]/, '*') }
|
|
386
|
+
points.each { |x, y| expected[y][x] = '*' }
|
|
387
|
+
expect(logical_ink(frame, full: true)).to eq(expected)
|
|
388
|
+
end
|
|
389
|
+
frames.each_cons(2) do |before, after|
|
|
390
|
+
a = before[:state][:active]
|
|
391
|
+
b = after[:state][:active]
|
|
392
|
+
if a && b
|
|
393
|
+
expect(before[:state][:stack]).to eq(after[:state][:stack])
|
|
394
|
+
expect(b.map(&:last).min - a.map(&:last).min).to be_between(0, 1)
|
|
395
|
+
expect((b.map(&:first).min - a.map(&:first).min).abs).to be <= 1
|
|
396
|
+
elsif a && after[:event] == :lock
|
|
397
|
+
stack = before[:state][:stack]
|
|
398
|
+
expect(a.any? { |x, y| y == ((size - 1) * 2) - 1 || stack[y + 1][x] != ' ' }).to be(true)
|
|
399
|
+
expect(after[:rows]).to eq(before[:rows])
|
|
400
|
+
elsif before[:event] == :clear && after[:event] == :collapse
|
|
401
|
+
lines = before[:state][:lines]
|
|
402
|
+
expect(lines.map { |y| logical_ink(before, full: true)[y] }).to all(eq('*' * size))
|
|
403
|
+
survivors = logical_ink(before, full: true).each_with_index.filter_map { |row, y| row unless lines.include?(y) }
|
|
404
|
+
expect(logical_ink(after, full: true)).to eq(Array.new(lines.length, ' ' * size) + survivors)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it 'moves a contiguous non-overlapping snake toward real food and grows only on pickup' do
|
|
411
|
+
(5..16).each do |size|
|
|
412
|
+
frames = replay(:snake, size)
|
|
413
|
+
states = frames.filter_map { |frame| frame[:state] if frame[:state][:body] }
|
|
414
|
+
expect(states.map { |state| state[:body].length }.max).to be > 6
|
|
415
|
+
states.each do |state|
|
|
416
|
+
body, food = state.values_at(:body, :food)
|
|
417
|
+
expect(body.uniq.length).to eq(body.length)
|
|
418
|
+
body.each do |x, y|
|
|
419
|
+
expect(x).to be_between(0, size - 1)
|
|
420
|
+
expect(y).to be_between(0, size - 2)
|
|
421
|
+
end
|
|
422
|
+
body.each_cons(2) { |a, b| expect(a.zip(b).sum { |x, y| (x - y).abs }).to eq(1) }
|
|
423
|
+
expect(body).not_to include(food) if food
|
|
424
|
+
end
|
|
425
|
+
frames.each do |frame|
|
|
426
|
+
body, food = frame[:state].values_at(:body, :food)
|
|
427
|
+
next unless body
|
|
428
|
+
|
|
429
|
+
expected = Array.new(size - 1) { ' ' * size }
|
|
430
|
+
(body + [food].compact).each { |x, y| expected[y][x] = '*' }
|
|
431
|
+
expect(logical_ink(frame)).to eq(expected)
|
|
432
|
+
end
|
|
433
|
+
frames.each_cons(2) do |before, after|
|
|
434
|
+
a = before[:state]
|
|
435
|
+
b = after[:state]
|
|
436
|
+
next unless a[:body] && b[:body] && a != b
|
|
437
|
+
|
|
438
|
+
expect(a[:body].first.zip(b[:body].first).sum { |x, y| (x - y).abs }).to eq(1)
|
|
439
|
+
if b[:body].first == a[:food]
|
|
440
|
+
expect(b[:body].drop(1)).to eq(a[:body])
|
|
441
|
+
else
|
|
442
|
+
expect(b[:body].drop(1)).to eq(a[:body].take(a[:body].length - 1))
|
|
443
|
+
expect(b[:food]).to eq(a[:food])
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it 'plays varied Pong rallies with legal reflections, paddle tracking, misses and scores' do
|
|
450
|
+
(5..16).each do |size|
|
|
451
|
+
frames = replay(:pong, size)
|
|
452
|
+
expect(frames.map { |frame| frame[:event] }).to include(:bounce, :score, :serve)
|
|
453
|
+
expect(frames.filter_map { |frame| frame[:state][:rally] }.max).to be >= 2
|
|
454
|
+
expect(frames.filter_map { |frame| frame[:state][:rally] }.max).to be >= 5 if size == 16
|
|
455
|
+
frames.each_cons(2) do |before, after|
|
|
456
|
+
a = before[:state]
|
|
457
|
+
b = after[:state]
|
|
458
|
+
next unless a[:paddles] && b[:paddles]
|
|
459
|
+
|
|
460
|
+
expect(a[:paddles].zip(b[:paddles]).map { |x, y| (x.round - y.round).abs }).to all(be <= 1)
|
|
461
|
+
if a[:ball] && b[:ball]
|
|
462
|
+
expect(a[:ball].zip(b[:ball]).map { |x, y| (x.round - y.round).abs }).to all(be <= 1)
|
|
463
|
+
expect(b[:ball][0]).to be_between(0, size - 1)
|
|
464
|
+
expect(b[:ball][1]).to be_between(0, size - 2)
|
|
465
|
+
if after[:event] == :bounce
|
|
466
|
+
side = b[:ball][0] < (size - 1) / 2.0 ? 0 : 1
|
|
467
|
+
expect(b[:ball][1] - b[:paddles][side]).to be_between(-0.25, 0.75)
|
|
468
|
+
expect(a[:ball][0]).to be_between(1, size - 2)
|
|
469
|
+
end
|
|
470
|
+
elsif a[:ball] && after[:event] == :score
|
|
471
|
+
expect(a[:ball][0] < 1 || a[:ball][0] > size - 2).to be(true)
|
|
472
|
+
expect(b[:scores].sum - a[:scores].sum).to eq(1)
|
|
473
|
+
elsif b[:ball]
|
|
474
|
+
expect(after[:event]).to eq(:serve)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
it 'returns deterministic named-color block cells without changing the ASCII API' do
|
|
481
|
+
rows = described_class.mini_cells(name: :falling_blocks, width: 5, height: 5)
|
|
482
|
+
expect(rows.length).to eq(5)
|
|
483
|
+
expect(rows.map(&:length)).to eq([5] * 5)
|
|
484
|
+
expect(rows.first.map { |cell| cell[:glyph] }.join).to eq(' PWN ')
|
|
485
|
+
expect(rows[1][1]).to eq(glyph: ' ', foreground: :blue, background: :black)
|
|
486
|
+
expect(described_class.mini_cells(name: :falling_blocks, width: 5, height: 5, frame: described_class::MINI_FRAME_COUNT)).to eq(rows)
|
|
487
|
+
expect(described_class.mini_frame(width: 5, height: 5)[1]).to eq(' ### ')
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
it 'bounds immutable replay caching and survives eviction, arbitrary seek and caller mutation' do
|
|
491
|
+
first = replay(:snake, 8)
|
|
492
|
+
expect(first.length).to eq(described_class::MINI_FRAME_COUNT)
|
|
493
|
+
expect(first).to be_frozen
|
|
494
|
+
expect { first[20][:rows][1].replace('bad') }.to raise_error(FrozenError)
|
|
495
|
+
expect { first[20][:state][:body][0][0] = 99 }.to raise_error(FrozenError)
|
|
496
|
+
expect(replay(:snake, 8)).to equal(first)
|
|
497
|
+
9.times { |seed| replay(:pong, 5, seed) }
|
|
498
|
+
expect(described_class.instance_variable_get(:@mini_replays).length).to eq(described_class::MINI_CACHE_LIMIT)
|
|
499
|
+
expect(replay(:snake, 8)).to eq(first)
|
|
500
|
+
[1799, -1, 921, 600, 6, 1800, -1800].each do |frame|
|
|
501
|
+
cells = described_class.mini_cells(name: :snake, width: 8, height: 8, frame: frame)
|
|
502
|
+
expect(cells.map { |row| row.map { |cell| cell[:glyph] }.join }).to eq(
|
|
503
|
+
first[frame % described_class::MINI_FRAME_COUNT][:rows].map { |row| row.chars.map { |pixel| described_class::MINI_PIXELS.fetch(pixel).first }.join }
|
|
504
|
+
)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
it 'has a visible reset and blank-to-blank seam rather than teleporting at replay wrap' do
|
|
509
|
+
described_class.mini_names.each do |name|
|
|
510
|
+
[5, 8, 16].each do |size|
|
|
511
|
+
frames = replay(name, size)
|
|
512
|
+
expect(frames.length).to eq(described_class::MINI_FRAME_COUNT)
|
|
513
|
+
expect(frames.last[:rows]).to eq(frames.first[:rows])
|
|
514
|
+
expect(frames[-size - 6][:event]).to eq(:reset)
|
|
515
|
+
frames.last(size).each_cons(2) do |before, after|
|
|
516
|
+
expect(before[:rows].zip(after[:rows]).count { |a, b| a != b }).to be <= 1
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
describe PWN::Banner do
|
|
525
|
+
describe '.mini_frame' do
|
|
526
|
+
it 'offers retro games with deterministic cycles and branding at every square size' do
|
|
527
|
+
expect(described_class.mini_names).to eq(%i[falling_blocks snake pong asteroids])
|
|
528
|
+
described_class.mini_names.each do |name|
|
|
529
|
+
(5..16).each do |size|
|
|
530
|
+
args = { name: name, width: size, height: size }
|
|
531
|
+
frames = Array.new(described_class::MINI_FRAME_COUNT) { |frame| described_class.mini_frame(**args, frame: frame) }
|
|
532
|
+
expect(frames.uniq.length).to be >= 4
|
|
533
|
+
expect(frames.flatten).to all(match(/\A[ -~]{#{size}}\z/))
|
|
534
|
+
expect(frames.map(&:length)).to all(eq(size))
|
|
535
|
+
expect(frames.map(&:first)).to all(include('PWN'))
|
|
536
|
+
expect(described_class.mini_frame(**args, frame: described_class::MINI_FRAME_COUNT)).to eq(frames.first)
|
|
537
|
+
expect(described_class.mini_frame(**args, name: name.to_s, frame: -1)).to eq(frames.last)
|
|
538
|
+
expect(described_class.mini_frame(**args, frame: 3)).to eq(frames[3])
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
it 'returns a compact printable ASCII wordmark without terminal side effects' do
|
|
544
|
+
lines = nil
|
|
545
|
+
expect { lines = described_class.mini_frame(frame: 0) }.not_to output.to_stdout
|
|
546
|
+
expect(lines.length).to eq(16)
|
|
547
|
+
expect(lines).to all(match(/\A[ -~]{16}\z/))
|
|
548
|
+
expect(lines.first).to include('PWN')
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
it 'clips safely for tiny panes and caps oversized requests at native dimensions' do
|
|
552
|
+
described_class.mini_names.each do |name|
|
|
553
|
+
[1, 3, 5, 8, 16].product([1, 3, 5, 8, 16]).each do |width, height|
|
|
554
|
+
rows = described_class.mini_frame(name: name, width: width, height: height)
|
|
555
|
+
expect(rows.length).to eq(height)
|
|
556
|
+
expect(rows.map(&:length)).to all(eq(width))
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
expect(described_class.mini_frame(width: 0)).to eq([])
|
|
560
|
+
expect(described_class.mini_frame(height: -1)).to eq([])
|
|
561
|
+
expect(described_class.mini_frame(width: -1)).to eq([])
|
|
562
|
+
expect(described_class.mini_frame(width: 10**20, height: 10**20)).to eq(described_class.mini_frame)
|
|
563
|
+
expect(described_class.mini_frame(name: nil, frame: nil, width: nil, height: nil)).to eq(described_class.mini_frame)
|
|
564
|
+
expect(described_class.mini_frame(width: '3', height: '2').map(&:length)).to eq([3, 3])
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
it 'returns independent rows and name lists without retaining caller mutations' do
|
|
568
|
+
original = described_class.mini_frame
|
|
569
|
+
changed = described_class.mini_frame
|
|
570
|
+
changed.first.replace('changed')
|
|
571
|
+
changed.clear
|
|
572
|
+
described_class.mini_names.clear
|
|
573
|
+
expect(described_class.mini_frame).to eq(original)
|
|
574
|
+
expect(described_class.mini_names.length).to eq(4)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
it 'keeps recognizable pieces, bodies and paddles in the smallest complete canvas' do
|
|
578
|
+
expect(described_class.mini_frame(name: :falling_blocks, width: 5, height: 5)).to eq(
|
|
579
|
+
[' PWN ', ' ### ', ' # ', '# #', '## ##']
|
|
580
|
+
)
|
|
581
|
+
expect(described_class.mini_frame(name: :snake, width: 5, height: 5)).to eq(
|
|
582
|
+
[' PWN ', '@ ', 'o * ', 'o ', 'oo ']
|
|
583
|
+
)
|
|
584
|
+
expect(described_class.mini_frame(name: :pong, width: 5, height: 5)).to eq(
|
|
585
|
+
[' PWN ', '|o: |', '| |', ' : ', ' ']
|
|
586
|
+
)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
it 'lands tetrominoes before flashing and clearing the completed rows' do
|
|
590
|
+
[5, 8, 16].each do |size|
|
|
591
|
+
[0, 30].each do |start|
|
|
592
|
+
args = { name: :falling_blocks, width: size, height: size }
|
|
593
|
+
expect(described_class.mini_frame(**args, frame: start + 24).last(2)).to eq(['#' * size] * 2)
|
|
594
|
+
expect(described_class.mini_frame(**args, frame: start + 26).last(2)).to eq(['=' * size] * 2)
|
|
595
|
+
expect(described_class.mini_frame(**args, frame: start + 29).last(2)).to eq([' ' * size] * 2)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
it 'moves snake and ball at most one cell per axis including the loop seam' do
|
|
601
|
+
{ snake: '@', pong: 'o' }.each do |name, glyph|
|
|
602
|
+
(5..16).each do |size|
|
|
603
|
+
positions = Array.new(described_class::MINI_FRAME_COUNT + 1) do |frame|
|
|
604
|
+
rows = described_class.mini_frame(name: name, frame: frame, width: size, height: size)
|
|
605
|
+
y = rows.index { |row| row.include?(glyph) }
|
|
606
|
+
[rows[y].index(glyph), y]
|
|
607
|
+
end
|
|
608
|
+
positions.each_cons(2) do |before, after|
|
|
609
|
+
expect((before[0] - after[0]).abs).to be <= 1
|
|
610
|
+
expect((before[1] - after[1]).abs).to be <= 1
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
it 'never aliases mutable rows even during the line-clear animation' do
|
|
617
|
+
described_class.mini_names.each do |name|
|
|
618
|
+
described_class::MINI_FRAME_COUNT.times do |frame|
|
|
619
|
+
rows = described_class.mini_frame(name: name, frame: frame)
|
|
620
|
+
expect(rows.map(&:object_id).uniq.length).to eq(rows.length)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
it 'never samples randomness, reads the clock, starts threads, sleeps, or prints' do
|
|
626
|
+
expect(Random).not_to receive(:rand)
|
|
627
|
+
expect(Time).not_to receive(:now)
|
|
628
|
+
expect(Process).not_to receive(:clock_gettime)
|
|
629
|
+
expect(Thread).not_to receive(:new)
|
|
630
|
+
expect(described_class).not_to receive(:sleep)
|
|
631
|
+
expect do
|
|
632
|
+
described_class.mini_names.each { |name| described_class.mini_frame(name: name, frame: 5) }
|
|
633
|
+
end.to output('').to_stdout.and output('').to_stderr
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
it 'rejects unknown artwork rather than silently randomizing it' do
|
|
637
|
+
expect { described_class.mini_frame(name: :missing) }.to raise_error(ArgumentError, /Unknown mini-banner/)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
it 'preserves the original static banner dispatch' do
|
|
641
|
+
modules = %i[Anon Bubble Cheshire CodeCave DontPanic ForkBomb FSociety JmpEsp Matrix Ninja OffTheAir Pirate Radare2 Radare2AI WhiteRabbit]
|
|
642
|
+
modules.each_with_index do |name, index|
|
|
643
|
+
expect(described_class.const_get(name)).to receive(:get).and_return(name.to_s)
|
|
644
|
+
expect(described_class.get(index: index + 1)).to eq(name.to_s)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
6
649
|
it 'should display information for authors' do
|
|
7
650
|
authors_response = PWN::Banner
|
|
8
651
|
expect(authors_response).to respond_to :authors
|