pwn 0.5.759 → 0.5.760
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 +1 -1
- data/lib/pwn/ai/agent/loop.rb +73 -18
- data/lib/pwn/banner.rb +157 -38
- data/lib/pwn/plugins/repl/ai_console.rb +57 -7
- data/lib/pwn/plugins/repl/ai_swarm.rb +1 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +1 -1
- data/spec/lib/pwn/banner_spec.rb +96 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +101 -0
- data/third_party/pwn_rdoc.jsonl +13 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2115a19977c0539855a44ecea57a2fb74bc48ae80ff3a713d2c08b95bdb04e55
|
|
4
|
+
data.tar.gz: 3c237b68883205d53dee7911e18be1e5b5454184f27c7d093e655e75611f5184
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01c9e98a32745f26186d2f664d62f7d1801eea05a56ffe149ea20b84aaef383d5d46d909e5be97247454cedad4781c3ce393783dcc64707a07bbd818a37872c5
|
|
7
|
+
data.tar.gz: 562ea7604c6e23cbe128864aca11338e9c60bd7d83bd5e54692c956f136f29f036e1fb7b63c6cdb9f0defd3168c616cfff5c9194f169b9df209094952e0ae66b
|
data/Gemfile
CHANGED
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -59,8 +59,78 @@ module PWN
|
|
|
59
59
|
# (:escalation_persona) — the local model still produces the final
|
|
60
60
|
# answer so Learning/Metrics stay attributed to :ollama.
|
|
61
61
|
module Loop
|
|
62
|
+
# Provider calls run on a child. Thread#raise does not wake a native read,
|
|
63
|
+
# so the owner waits in Ruby and leaves within one poll on cancel.
|
|
64
|
+
class ProviderWindow
|
|
65
|
+
def request_model_cancel
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@cancel_requested = true
|
|
68
|
+
if @phase == :model && !@model_signal_sent
|
|
69
|
+
@model_signal_sent = true
|
|
70
|
+
@owner.raise(Steering::ModelCancelled)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def model(messages:, &block)
|
|
76
|
+
result = nil
|
|
77
|
+
worker = nil
|
|
78
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :never) do
|
|
79
|
+
checkpoint(messages: messages, phase: :model)
|
|
80
|
+
raise Steering::ModelCancelled if cancel_requested?
|
|
81
|
+
|
|
82
|
+
worker = provider_thread(&block)
|
|
83
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :immediate) do
|
|
84
|
+
loop do
|
|
85
|
+
raise Steering::ModelCancelled if cancel_requested?
|
|
86
|
+
|
|
87
|
+
break if worker.join(0.05)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
result = worker.value
|
|
91
|
+
ensure
|
|
92
|
+
abandon_provider(worker)
|
|
93
|
+
@mutex.synchronize { @phase = :boundary }
|
|
94
|
+
begin
|
|
95
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :immediate) { nil }
|
|
96
|
+
rescue Steering::ModelCancelled
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
result
|
|
101
|
+
rescue Steering::ModelCancelled
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def cancel_requested?
|
|
108
|
+
@mutex.synchronize { @cancel_requested }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def provider_thread(&block)
|
|
112
|
+
parent = Thread.current
|
|
113
|
+
locals = parent.keys.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, parent[key]] }
|
|
114
|
+
variables = parent.thread_variables.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, parent.thread_variable_get(key)] }
|
|
115
|
+
Thread.new do
|
|
116
|
+
Thread.current.report_on_exception = false
|
|
117
|
+
locals.each { |key, value| Thread.current[key] = value }
|
|
118
|
+
variables.each { |key, value| Thread.current.thread_variable_set(key, value) }
|
|
119
|
+
Thread.handle_interrupt(Steering::ModelCancelled => :never, &block)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def abandon_provider(worker)
|
|
124
|
+
return unless worker&.alive?
|
|
125
|
+
|
|
126
|
+
worker.raise(Steering::ModelCancelled)
|
|
127
|
+
worker.join(0.2)
|
|
128
|
+
worker.kill if worker.alive?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
62
132
|
# Request-owned input broker; only the provider window is interruptible.
|
|
63
|
-
class Steering
|
|
133
|
+
class Steering < ProviderWindow
|
|
64
134
|
INPUT_LOCK = Mutex.new
|
|
65
135
|
INPUT_OWNERS = {} # rubocop:disable Style/MutableConstant -- guarded by INPUT_LOCK
|
|
66
136
|
|
|
@@ -80,12 +150,14 @@ module PWN
|
|
|
80
150
|
attr_reader :reader
|
|
81
151
|
|
|
82
152
|
def initialize(input:, output:)
|
|
153
|
+
super()
|
|
83
154
|
@input = input
|
|
84
155
|
@output = output
|
|
85
156
|
@owner = Thread.current
|
|
86
157
|
@mutex = Mutex.new
|
|
87
158
|
@pending = []
|
|
88
159
|
@phase = :boundary
|
|
160
|
+
@cancel_requested = false
|
|
89
161
|
end
|
|
90
162
|
|
|
91
163
|
def submit(text)
|
|
@@ -127,23 +199,6 @@ module PWN
|
|
|
127
199
|
raise Restart.new(messages, instructions)
|
|
128
200
|
end
|
|
129
201
|
|
|
130
|
-
def model(messages:, &block)
|
|
131
|
-
# Drain an in-flight signal inside this rescue, never in a tool.
|
|
132
|
-
Thread.handle_interrupt(ModelCancelled => :never) do
|
|
133
|
-
checkpoint(messages: messages, phase: :model)
|
|
134
|
-
Thread.handle_interrupt(ModelCancelled => :immediate, &block)
|
|
135
|
-
ensure
|
|
136
|
-
@mutex.synchronize { @phase = :boundary }
|
|
137
|
-
begin
|
|
138
|
-
Thread.handle_interrupt(ModelCancelled => :immediate) { nil }
|
|
139
|
-
rescue ModelCancelled
|
|
140
|
-
nil
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
rescue ModelCancelled
|
|
144
|
-
nil
|
|
145
|
-
end
|
|
146
|
-
|
|
147
202
|
def with_reader
|
|
148
203
|
require 'io/console'
|
|
149
204
|
input_id = @input.fileno
|
data/lib/pwn/banner.rb
CHANGED
|
@@ -187,6 +187,59 @@ module PWN
|
|
|
187
187
|
end
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
+
# Caller-owned simulation: yields on the UI thread, never reads input or
|
|
191
|
+
# accumulates a replay. The caller supplies the clock and normalized keys.
|
|
192
|
+
class MiniGame
|
|
193
|
+
def initialize(name:, width:, height:, seed: MINI_SEED)
|
|
194
|
+
@name = name.to_s
|
|
195
|
+
raise ArgumentError, "Unknown mini-banner: #{name}" unless Banner.mini_names.map(&:to_s).include?(@name)
|
|
196
|
+
|
|
197
|
+
limit = @name == 'falling_blocks' ? 64 : MINI_WIDTH
|
|
198
|
+
@width = width.clamp(5, limit)
|
|
199
|
+
@height = height.clamp(5, limit)
|
|
200
|
+
@keys = []
|
|
201
|
+
@simulation = Fiber.new do
|
|
202
|
+
options = { frames: self, width: @width, height: @name == 'falling_blocks' ? @height * 2 : @height,
|
|
203
|
+
rng: Random.new(seed), player: self }
|
|
204
|
+
Banner.send("mini_simulate_#{@name}", options)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def key(key)
|
|
209
|
+
@keys << key if @keys.length < 16
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def controls
|
|
213
|
+
keys = @keys
|
|
214
|
+
@keys = []
|
|
215
|
+
keys
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def step
|
|
219
|
+
@simulation.resume
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def cells(frame)
|
|
223
|
+
rows = frame[:rows].drop(1)
|
|
224
|
+
rows = Banner.send(:mini_block_board, board: rows) if @name == 'falling_blocks'
|
|
225
|
+
rows = Banner.send(:mini_quarter_board, board: rows, name: @name) if @name == 'snake'
|
|
226
|
+
rows.map do |row|
|
|
227
|
+
row.chars.map do |pixel|
|
|
228
|
+
glyph, foreground, background = MINI_PIXELS.fetch(pixel)
|
|
229
|
+
{ glyph: glyph.dup, foreground: foreground, background: background || :black }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def <<(frame)
|
|
235
|
+
Fiber.yield(frame)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def length
|
|
239
|
+
0
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
190
243
|
# Six immutable, compact replays bound memory even when callers resize/reseed.
|
|
191
244
|
private_class_method def self.mini_replay(opts = {})
|
|
192
245
|
key = [opts[:name].to_s.dup.freeze, opts[:width], opts[:height], opts[:seed]].freeze
|
|
@@ -347,56 +400,99 @@ module PWN
|
|
|
347
400
|
bag = (0...MINI_SHAPES.length).to_a.shuffle(random: rng) if bag.empty?
|
|
348
401
|
type = bag.shift
|
|
349
402
|
shape = MINI_SHAPES[type]
|
|
350
|
-
landing = mini_landing(board: board, shape: shape, rng: rng)
|
|
403
|
+
landing = opts[:player] ? {} : mini_landing(board: board, shape: shape, rng: rng)
|
|
351
404
|
x = (width - shape.map(&:first).max - 1) / 2
|
|
352
405
|
unless landing && mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
353
406
|
mini_reset_board(frames: frames, board: board)
|
|
354
407
|
board = Array.new(height) { ' ' * width }
|
|
355
408
|
next
|
|
356
409
|
end
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
shape =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
if opts[:player]
|
|
411
|
+
shape, x, y = mini_play_piece(board: board, type: type, frames: frames, player: opts[:player])
|
|
412
|
+
landing = { x: x, y: y }
|
|
413
|
+
else
|
|
414
|
+
shape, x = mini_block_plan(board: board, shape: shape, x: x, landing: landing, frames: frames, type: type)
|
|
415
|
+
unless shape
|
|
416
|
+
mini_reset_board(frames: frames, board: board)
|
|
417
|
+
board = Array.new(height) { ' ' * width }
|
|
418
|
+
next
|
|
366
419
|
end
|
|
367
420
|
end
|
|
368
|
-
until x == landing[:x] || blocked
|
|
369
|
-
x += landing[:x] <=> x
|
|
370
|
-
blocked = !mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
371
|
-
plan << [shape, x, 0] unless blocked
|
|
372
|
-
end
|
|
373
|
-
if blocked
|
|
374
|
-
mini_reset_board(frames: frames, board: board)
|
|
375
|
-
board = Array.new(height) { ' ' * width }
|
|
376
|
-
next
|
|
377
|
-
end
|
|
378
|
-
(1..landing[:y]).each { |y| plan << [shape, x, y] }
|
|
379
421
|
color = %w[c y m b r g g][type]
|
|
380
|
-
plan.each do |piece, px, py|
|
|
381
|
-
picture = board.map(&:dup)
|
|
382
|
-
active = piece.map { |dx, dy| [px + dx, py + dy] }
|
|
383
|
-
active.each { |ax, ay| picture[ay][ax] = color }
|
|
384
|
-
mini_emit(frames: frames, board: picture,
|
|
385
|
-
state: { active: active, stack: board.map(&:dup), type: type })
|
|
386
|
-
end
|
|
387
422
|
shape.each { |dx, dy| board[landing[:y] + dy][x + dx] = color }
|
|
388
423
|
mini_emit(frames: frames, board: board, event: :lock, ticks: 2)
|
|
389
|
-
full = board.each_index.reject { |
|
|
424
|
+
full = board.each_index.reject { |row| board[row].include?(' ') }
|
|
390
425
|
next if full.empty?
|
|
391
426
|
|
|
392
427
|
flash = board.map(&:dup)
|
|
393
|
-
full.each { |
|
|
428
|
+
full.each { |row| flash[row] = 'w' * width }
|
|
394
429
|
mini_emit(frames: frames, board: flash, event: :clear, state: { lines: full }, ticks: 3)
|
|
395
|
-
board = Array.new(full.length) { ' ' * width } + board.each_with_index.filter_map { |row,
|
|
430
|
+
board = Array.new(full.length) { ' ' * width } + board.each_with_index.filter_map { |row, index| row unless full.include?(index) }
|
|
396
431
|
mini_emit(frames: frames, board: board, event: :collapse, ticks: 2)
|
|
397
432
|
end
|
|
398
433
|
end
|
|
399
434
|
|
|
435
|
+
private_class_method def self.mini_block_plan(opts = {})
|
|
436
|
+
board, shape, x, landing, frames, type = opts.values_at(:board, :shape, :x, :landing, :frames, :type)
|
|
437
|
+
rotations = mini_rotations(shape: shape)
|
|
438
|
+
plan = [[shape, x, 0]]
|
|
439
|
+
blocked = false
|
|
440
|
+
(1..landing[:rotation]).each do |rotation|
|
|
441
|
+
shape = rotations[rotation]
|
|
442
|
+
if mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
443
|
+
plan << [shape, x, 0]
|
|
444
|
+
else
|
|
445
|
+
blocked = true
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
until x == landing[:x] || blocked
|
|
449
|
+
x += landing[:x] <=> x
|
|
450
|
+
blocked = !mini_fits?(board: board, shape: shape, x: x, y: 0)
|
|
451
|
+
plan << [shape, x, 0] unless blocked
|
|
452
|
+
end
|
|
453
|
+
return if blocked
|
|
454
|
+
|
|
455
|
+
(1..landing[:y]).each { |y| plan << [shape, x, y] }
|
|
456
|
+
color = %w[c y m b r g g][type]
|
|
457
|
+
plan.each do |piece, px, py|
|
|
458
|
+
picture = board.map(&:dup)
|
|
459
|
+
active = piece.map { |dx, dy| [px + dx, py + dy] }
|
|
460
|
+
active.each { |ax, ay| picture[ay][ax] = color }
|
|
461
|
+
mini_emit(frames: frames, board: picture,
|
|
462
|
+
state: { active: active, stack: board.map(&:dup), type: type })
|
|
463
|
+
end
|
|
464
|
+
[shape, x]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
private_class_method def self.mini_play_piece(opts = {})
|
|
468
|
+
board, type, frames, player = opts.values_at(:board, :type, :frames, :player)
|
|
469
|
+
shape = MINI_SHAPES[type]
|
|
470
|
+
x = (board.first.length - shape.map(&:first).max - 1) / 2
|
|
471
|
+
y = 0
|
|
472
|
+
tick = 0
|
|
473
|
+
loop do
|
|
474
|
+
active = shape.map { |dx, dy| [x + dx, y + dy] }
|
|
475
|
+
picture = board.map(&:dup)
|
|
476
|
+
active.each { |ax, ay| picture[ay][ax] = %w[c y m b r g g][type] }
|
|
477
|
+
mini_emit(frames: frames, board: picture, state: { active: active, stack: board.map(&:dup), type: type })
|
|
478
|
+
keys = player.controls
|
|
479
|
+
keys.each do |key|
|
|
480
|
+
candidate = key == ' ' ? mini_rotations(shape: shape).fetch(1, shape) : shape
|
|
481
|
+
nx = x + { left: -1, right: 1 }.fetch(key, 0)
|
|
482
|
+
next unless mini_fits?(board: board, shape: candidate, x: nx, y: y)
|
|
483
|
+
|
|
484
|
+
shape = candidate
|
|
485
|
+
x = nx
|
|
486
|
+
end
|
|
487
|
+
tick += 1
|
|
488
|
+
next unless keys.include?(:down) || (tick % 5).zero?
|
|
489
|
+
break unless mini_fits?(board: board, shape: shape, x: x, y: y + 1)
|
|
490
|
+
|
|
491
|
+
y += 1
|
|
492
|
+
end
|
|
493
|
+
[shape, x, y]
|
|
494
|
+
end
|
|
495
|
+
|
|
400
496
|
private_class_method def self.mini_reset_board(opts = {})
|
|
401
497
|
board = opts[:board].map { |row| row.gsub(/[^ ]/, 'w') }
|
|
402
498
|
mini_emit(frames: opts[:frames], board: board, event: :game_over, ticks: 8)
|
|
@@ -442,19 +538,31 @@ module PWN
|
|
|
442
538
|
height = opts[:height]
|
|
443
539
|
rng = opts[:rng]
|
|
444
540
|
spaces = (0...height).flat_map { |y| (0...width).map { |x| [x, y] } }
|
|
541
|
+
player = opts[:player]
|
|
445
542
|
while frames.length < MINI_FRAME_COUNT
|
|
446
543
|
y = rng.rand(height)
|
|
447
544
|
body = [[2, y], [1, y], [0, y]]
|
|
545
|
+
direction = [1, 0]
|
|
448
546
|
food = (spaces - body).sample(random: rng)
|
|
449
547
|
steps = 0
|
|
450
548
|
loop do
|
|
451
549
|
board = Array.new(height) { ' ' * width }
|
|
452
550
|
body.each_with_index { |(x, row), index| board[row][x] = index.zero? ? 'y' : 'g' }
|
|
453
551
|
board[food[1]][food[0]] = 'f' if food
|
|
454
|
-
mini_emit(frames: frames, board: board, ticks: 2, state: { body: body.map(&:dup), food: food&.dup })
|
|
552
|
+
mini_emit(frames: frames, board: board, ticks: player ? 1 : 2, state: { body: body.map(&:dup), food: food&.dup })
|
|
455
553
|
break if frames.length >= MINI_FRAME_COUNT
|
|
456
554
|
|
|
457
|
-
|
|
555
|
+
if player
|
|
556
|
+
turn = player.controls.filter_map { |key| { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[key] }.find do |dx, dy|
|
|
557
|
+
[dx, dy] != direction && [dx, dy] != direction.map(&:-@)
|
|
558
|
+
end
|
|
559
|
+
direction = turn if turn
|
|
560
|
+
target = [body.first[0] + direction[0], body.first[1] + direction[1]]
|
|
561
|
+
occupied = target == food ? body : body.take(body.length - 1)
|
|
562
|
+
target = nil unless spaces.include?(target) && !occupied.include?(target)
|
|
563
|
+
elsif food
|
|
564
|
+
target = mini_snake_route(body: body, food: food, width: width, height: height, rng: rng)
|
|
565
|
+
end
|
|
458
566
|
if !food || !target || steps >= width * height * 4
|
|
459
567
|
mini_reset_board(frames: frames, board: board)
|
|
460
568
|
break
|
|
@@ -477,6 +585,7 @@ module PWN
|
|
|
477
585
|
height = opts[:height]
|
|
478
586
|
rng = opts[:rng]
|
|
479
587
|
paddles = [(height - 1) / 2.0, (height - 1) / 2.0]
|
|
588
|
+
player = opts[:player]
|
|
480
589
|
scores = [0, 0]
|
|
481
590
|
while frames.length < MINI_FRAME_COUNT
|
|
482
591
|
x = (width - 1) / 2.0
|
|
@@ -485,9 +594,14 @@ module PWN
|
|
|
485
594
|
vy = ((rng.rand * 0.4) + 0.1) * (rng.rand(2).zero? ? -1 : 1)
|
|
486
595
|
bias = Array.new(2) { rng.rand(5).zero? ? rng.rand(-2..2) : rng.rand - 0.5 }
|
|
487
596
|
rally = 0
|
|
488
|
-
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: :serve, ticks: 16)
|
|
597
|
+
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: :serve, ticks: player ? 1 : 16)
|
|
489
598
|
loop do
|
|
599
|
+
player&.controls&.each do |key|
|
|
600
|
+
paddles[0] = (paddles[0] + { up: -0.5, down: 0.5 }.fetch(key, 0)).clamp(0, height - 1)
|
|
601
|
+
end
|
|
490
602
|
paddles.each_index do |side|
|
|
603
|
+
next if player && side.zero?
|
|
604
|
+
|
|
491
605
|
incoming = side.zero? ? vx.negative? : vx.positive?
|
|
492
606
|
target = incoming ? y - 0.25 + bias[side] : (height - 1) / 2.0
|
|
493
607
|
speed = incoming ? 0.275 : 0.11
|
|
@@ -519,7 +633,7 @@ module PWN
|
|
|
519
633
|
y = ny
|
|
520
634
|
if x.negative? || x > width - 1
|
|
521
635
|
scores[x.negative? ? 1 : 0] += 1
|
|
522
|
-
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: nil, scores: scores, event: :score, ticks: 20)
|
|
636
|
+
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: nil, scores: scores, event: :score, ticks: player ? 1 : 20)
|
|
523
637
|
break
|
|
524
638
|
end
|
|
525
639
|
mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: event, rally: rally)
|
|
@@ -594,13 +708,18 @@ module PWN
|
|
|
594
708
|
end
|
|
595
709
|
angle = Math.atan2(mini_delta(from: ship[1], to: target[1], span: height), mini_delta(from: ship[0], to: target[0], span: width))
|
|
596
710
|
turn = mini_delta(from: ship[2], to: angle, span: Math::PI * 2).clamp(-0.1, 0.1)
|
|
711
|
+
keys = opts[:player]&.controls
|
|
712
|
+
turn = keys.sum { |key| { left: -0.2, right: 0.2 }.fetch(key, 0) } if keys
|
|
597
713
|
ship[2] = (ship[2] + turn) % (Math::PI * 2)
|
|
598
|
-
|
|
714
|
+
power = tick % 80 < 24 ? 1 : 0
|
|
715
|
+
power = keys.sum { |key| { up: 1, down: -1 }.fetch(key, 0) }.clamp(-1, 1) if keys
|
|
716
|
+
thrust = !power.zero?
|
|
599
717
|
[Math.cos(ship[2]), Math.sin(ship[2])].each_with_index do |direction, axis|
|
|
600
|
-
velocity[axis] = ((velocity[axis] * 0.99) + (
|
|
718
|
+
velocity[axis] = ((velocity[axis] * 0.99) + (direction * 0.006 * power)).clamp(-0.15, 0.15)
|
|
601
719
|
ship[axis] = (ship[axis] + velocity[axis]) % (axis.zero? ? width : height)
|
|
602
720
|
end
|
|
603
|
-
|
|
721
|
+
fire = keys ? keys.include?(' ') : (tick % 12).zero?
|
|
722
|
+
shots << [ship[0], ship[1], Math.cos(ship[2]) * 0.36, Math.sin(ship[2]) * 0.36, 24] if fire
|
|
604
723
|
shots.each do |shot|
|
|
605
724
|
shot[0] = (shot[0] + shot[2]) % width
|
|
606
725
|
shot[1] = (shot[1] + shot[3]) % height
|
|
@@ -86,6 +86,7 @@ module PWN
|
|
|
86
86
|
|
|
87
87
|
def stop
|
|
88
88
|
@stopping = true
|
|
89
|
+
request_model_cancel
|
|
89
90
|
submit('Stop the active request.')
|
|
90
91
|
end
|
|
91
92
|
|
|
@@ -524,13 +525,12 @@ module PWN
|
|
|
524
525
|
|
|
525
526
|
def cancel
|
|
526
527
|
if busy?
|
|
527
|
-
|
|
528
|
-
|
|
528
|
+
first = !@cancelling
|
|
529
529
|
@cancelling = true
|
|
530
530
|
@control&.stop
|
|
531
531
|
@swarm&.cancel_all
|
|
532
532
|
@tool_input.close unless @tool_input.closed?
|
|
533
|
-
add(:warning, 'Cancellation requested. Waiting for
|
|
533
|
+
add(:warning, 'Cancellation requested. Waiting for a safe boundary; work already started is not undone.') if first
|
|
534
534
|
else
|
|
535
535
|
@editor.replace('')
|
|
536
536
|
end
|
|
@@ -681,6 +681,7 @@ module PWN
|
|
|
681
681
|
def handle(key)
|
|
682
682
|
return if key.nil?
|
|
683
683
|
return handle_system_role(key) if @role_editor && (!@too_small || ["\e", "\u0003", "\u0004", :eof].include?(key))
|
|
684
|
+
return cancel if key == "\u0003" && busy?
|
|
684
685
|
|
|
685
686
|
return if @too_small && ![:eof, "\u0003", "\u0004"].include?(key)
|
|
686
687
|
return cancel if @too_small && key == "\u0003" && !@model_prompt
|
|
@@ -714,7 +715,10 @@ module PWN
|
|
|
714
715
|
|
|
715
716
|
if ["\u0018", "\u0014"].include?(key)
|
|
716
717
|
@menu = nil
|
|
717
|
-
|
|
718
|
+
panes = @header_pane_height ? %i[mission session animation] : %i[mission session]
|
|
719
|
+
@focus = panes[(panes.index(@focus).to_i + 1) % panes.length]
|
|
720
|
+
@banner_game&.controls
|
|
721
|
+
@banner_game_tick = nil
|
|
718
722
|
return
|
|
719
723
|
end
|
|
720
724
|
|
|
@@ -726,6 +730,8 @@ module PWN
|
|
|
726
730
|
@menu = nil
|
|
727
731
|
return
|
|
728
732
|
end
|
|
733
|
+
return handle_animation(key) if @focus == :animation && ![:eof, "\u0003", "\u0004"].include?(key)
|
|
734
|
+
|
|
729
735
|
case key
|
|
730
736
|
when :eof, "\u0004"
|
|
731
737
|
@leaving = true
|
|
@@ -794,6 +800,24 @@ module PWN
|
|
|
794
800
|
end
|
|
795
801
|
end
|
|
796
802
|
|
|
803
|
+
def handle_animation(key)
|
|
804
|
+
if key == :paste_start
|
|
805
|
+
@focus = :mission
|
|
806
|
+
@paste = true
|
|
807
|
+
@banner_game&.controls
|
|
808
|
+
elsif [:up, :down, :left, :right, ' '].include?(key)
|
|
809
|
+
@banner_game&.key(key)
|
|
810
|
+
elsif ["\r", "\n", "\e", :newline].include?(key)
|
|
811
|
+
@focus = :mission
|
|
812
|
+
@banner_game&.controls
|
|
813
|
+
elsif key.is_a?(String) && key.ord >= 32
|
|
814
|
+
@focus = :mission
|
|
815
|
+
@banner_game&.controls
|
|
816
|
+
@editor.insert(key)
|
|
817
|
+
refresh_completion
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
797
821
|
def open_system_role
|
|
798
822
|
return add(:warning, 'System role cannot change while a request or swarm job is running.') if busy?
|
|
799
823
|
|
|
@@ -1201,6 +1225,7 @@ module PWN
|
|
|
1201
1225
|
end
|
|
1202
1226
|
cadence = cells ? @banner_frame_seconds : PWN::Banner::MINI_FRAME_SECONDS
|
|
1203
1227
|
frame = ((now - @banner_started) / cadence).floor % PWN::Banner::MINI_FRAME_COUNT
|
|
1228
|
+
return playable_banner(size, now) if cells && @focus == :animation
|
|
1204
1229
|
return PWN::Banner.mini_cells(name: @banner_name, frame: frame, width: size, height: size, seed: @banner_seed, branding: false) if cells
|
|
1205
1230
|
|
|
1206
1231
|
art = PWN::Banner.mini_frame(name: @banner_name, frame: frame, width: size, height: size, branding: false)
|
|
@@ -1210,6 +1235,21 @@ module PWN
|
|
|
1210
1235
|
canvas
|
|
1211
1236
|
end
|
|
1212
1237
|
|
|
1238
|
+
def playable_banner(size, now)
|
|
1239
|
+
key = [@banner_session, @banner_name, size, @banner_seed]
|
|
1240
|
+
if @banner_game_key != key
|
|
1241
|
+
@banner_game_key = key
|
|
1242
|
+
@banner_game = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed)
|
|
1243
|
+
@banner_game_tick = nil
|
|
1244
|
+
end
|
|
1245
|
+
tick = (now / @banner_frame_seconds).floor
|
|
1246
|
+
if @banner_game_tick != tick
|
|
1247
|
+
@banner_game_cells = @banner_game.cells(@banner_game.step)
|
|
1248
|
+
@banner_game_tick = tick
|
|
1249
|
+
end
|
|
1250
|
+
@banner_game_cells
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1213
1253
|
def draw_banner(size)
|
|
1214
1254
|
cells = banner_frame(size, cells: true)
|
|
1215
1255
|
top = [(size - cells.length) / 2, 0].max
|
|
@@ -1267,6 +1307,11 @@ module PWN
|
|
|
1267
1307
|
|
|
1268
1308
|
def footer_text
|
|
1269
1309
|
text = '/ menu · ^C=cancel · ^D=back · ^L=clear · ^T=toggle pane · ^S=swarm · ^R=search ^O=operations · ↑↓ history · Shift+Enter=newline · Enter=send'
|
|
1310
|
+
if @focus == :animation
|
|
1311
|
+
controls = { pong: '↑↓ paddle', snake: '↑↓←→ steer', falling_blocks: '←→ move · ↓ faster · Space=rotate',
|
|
1312
|
+
asteroids: '←→ turn · ↑↓ thrust/reverse · Space=fire' }.fetch(@banner_name, '↑↓←→ play')
|
|
1313
|
+
return text.sub('↑↓ history', controls).sub('Shift+Enter=newline · Enter=send', 'Enter/Esc=draft')
|
|
1314
|
+
end
|
|
1270
1315
|
@focus == :session ? text.sub('↑↓ history', '↑↓ scroll · HOME · PGUP · PGDN · END') : text
|
|
1271
1316
|
end
|
|
1272
1317
|
|
|
@@ -1295,6 +1340,7 @@ module PWN
|
|
|
1295
1340
|
visible.each_with_index { |spans, index| put_spans(2 + index, @header_text_column, spans) }
|
|
1296
1341
|
if @header_pane_height
|
|
1297
1342
|
box(0, 0, height, height, '')
|
|
1343
|
+
mark_active(0, 0, height, 'PLAY') if @focus == :animation
|
|
1298
1344
|
draw_banner(height - 2)
|
|
1299
1345
|
end
|
|
1300
1346
|
height
|
|
@@ -1338,6 +1384,8 @@ module PWN
|
|
|
1338
1384
|
end
|
|
1339
1385
|
engine = PWN::Env.dig(:ai, :active).to_s
|
|
1340
1386
|
model = REPL.pwn_ai_engine_model(engine: engine)
|
|
1387
|
+
header_layout(engine, model)
|
|
1388
|
+
@focus = :mission if @focus == :animation && !@header_pane_height
|
|
1341
1389
|
footer, compose_height, footer_height = layout_chrome
|
|
1342
1390
|
@compose_height = compose_height
|
|
1343
1391
|
@footer_height = footer_height
|
|
@@ -1359,9 +1407,9 @@ module PWN
|
|
|
1359
1407
|
rows.slice(start, @page_size).to_a.each_with_index { |(color, row), index| put(header_height + 1 + index, 2, row, color) }
|
|
1360
1408
|
draw_sidebar(header_height, pane_width + 1, timeline_height) if sidebar.positive?
|
|
1361
1409
|
compose_y = @height - compose_height - footer_height
|
|
1362
|
-
mission_title = @focus == :
|
|
1410
|
+
mission_title = @focus == :mission ? 'MISSION CONTROL · active' : 'MISSION CONTROL'
|
|
1363
1411
|
box(compose_y, 0, compose_height, @width - 1, mission_title)
|
|
1364
|
-
mark_active(compose_y, 0, @width - 1, mission_title) if @focus
|
|
1412
|
+
mark_active(compose_y, 0, @width - 1, mission_title) if @focus == :mission
|
|
1365
1413
|
draw_composer(compose_y, height: compose_height)
|
|
1366
1414
|
footer_top = @height - footer_height
|
|
1367
1415
|
footer.first(footer_height).each_with_index { |line, index| put(footer_top + index, 1, line, tone(:footer)) }
|
|
@@ -1370,7 +1418,9 @@ module PWN
|
|
|
1370
1418
|
draw_details(engine, model) if @details
|
|
1371
1419
|
draw_swarm if @workspace
|
|
1372
1420
|
draw_system_role if @role_editor
|
|
1373
|
-
if @focus == :
|
|
1421
|
+
if @focus == :animation && !(@details || @workspace || @role_editor || @model_prompt)
|
|
1422
|
+
@screen.setpos(1, 1)
|
|
1423
|
+
elsif @focus == :session && !(@details || @workspace || @role_editor)
|
|
1374
1424
|
@screen.setpos(header_height + 1, 2)
|
|
1375
1425
|
elsif @role_editor || !(@details || @workspace)
|
|
1376
1426
|
@screen.setpos(*@cursor_position)
|
data/lib/pwn/version.rb
CHANGED
|
@@ -228,7 +228,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
228
228
|
calls = []
|
|
229
229
|
owner = Thread.current
|
|
230
230
|
allow(described_class).to receive(:call_engine) do |opts|
|
|
231
|
-
expect(Thread.current).
|
|
231
|
+
expect(Thread.current).not_to eq(owner)
|
|
232
232
|
calls << Marshal.load(Marshal.dump(opts[:messages]))
|
|
233
233
|
if calls.length == 1
|
|
234
234
|
entered << true
|
data/spec/lib/pwn/banner_spec.rb
CHANGED
|
@@ -25,7 +25,103 @@ describe PWN::Banner do
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe PWN::Banner::MiniGame do
|
|
31
|
+
it 'renders bounded, unbranded colored frames without accumulating a replay' do
|
|
32
|
+
PWN::Banner.mini_names.product([5, 8, 16]).each do |name, size|
|
|
33
|
+
game = described_class.new(name: name, width: size, height: size)
|
|
34
|
+
40.times do
|
|
35
|
+
frame = game.step
|
|
36
|
+
cells = game.cells(frame)
|
|
37
|
+
expect(cells.length).to eq(size)
|
|
38
|
+
expect(cells.map(&:length)).to all(eq(size))
|
|
39
|
+
expect(cells.flatten.map { |cell| cell[:glyph] }.join).not_to match(/[PWN]/)
|
|
40
|
+
expect(cells.flatten).to all(include(:foreground, :background))
|
|
41
|
+
end
|
|
42
|
+
expect(game.length).to eq(0)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'lets the player move only the left Pong paddle while the ball and opponent keep playing' do
|
|
47
|
+
game = PWN::Banner::MiniGame.new(name: :pong, width: 8, height: 8, seed: 73)
|
|
48
|
+
first = game.step
|
|
49
|
+
game.key(:up)
|
|
50
|
+
moved = game.step
|
|
51
|
+
expect(moved[:state][:paddles][0]).to be < first[:state][:paddles][0]
|
|
52
|
+
expect(moved[:state][:ball]).not_to eq(first[:state][:ball])
|
|
53
|
+
game.key(:down)
|
|
54
|
+
expect(game.step[:state][:paddles][0]).to eq(first[:state][:paddles][0])
|
|
55
|
+
game.key(:left)
|
|
56
|
+
expect(game.step[:state][:paddles][0]).to eq(first[:state][:paddles][0])
|
|
57
|
+
100.times do
|
|
58
|
+
game.key(:up)
|
|
59
|
+
game.step
|
|
60
|
+
end
|
|
61
|
+
expect(game.step[:state][:paddles][0]).to eq(0)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'steers Snake with arrows without reversing into its neck or changing direction twice per tick' do
|
|
65
|
+
game = PWN::Banner::MiniGame.new(name: :snake, width: 16, height: 16, seed: 73)
|
|
66
|
+
first = game.step[:state]
|
|
67
|
+
game.key(:left)
|
|
68
|
+
moved = game.step[:state]
|
|
69
|
+
expect(moved[:body].first).to eq([first[:body][0][0] + 1, first[:body][0][1]])
|
|
70
|
+
turn = moved[:body][0][1].positive? ? :up : :down
|
|
71
|
+
game.key(turn)
|
|
72
|
+
game.key(:left)
|
|
73
|
+
turned = game.step[:state]
|
|
74
|
+
expect(turned[:body].first).to eq([moved[:body][0][0], moved[:body][0][1] + (turn == :up ? -1 : 1)])
|
|
75
|
+
game.key(:left)
|
|
76
|
+
expect(game.step[:state][:body].first[0]).to eq(turned[:body][0][0] - 1)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'moves Tetris sideways, rotates with space and soft drops one row with down' do
|
|
80
|
+
game = PWN::Banner::MiniGame.new(name: :falling_blocks, width: 8, height: 8, seed: 73)
|
|
81
|
+
first = game.step[:state]
|
|
82
|
+
game.key(:left)
|
|
83
|
+
left = game.step[:state]
|
|
84
|
+
expect(left[:active]).to eq(first[:active].map { |x, y| [x - 1, y] })
|
|
85
|
+
game.key(:right)
|
|
86
|
+
expect(game.step[:state][:active]).to eq(first[:active])
|
|
87
|
+
game.key(' ')
|
|
88
|
+
rotated = game.step[:state]
|
|
89
|
+
expect(rotated[:active]).not_to eq(first[:active])
|
|
90
|
+
expect(rotated[:active].map(&:last).min).to eq(0)
|
|
91
|
+
game.key(:down)
|
|
92
|
+
dropped = game.step[:state]
|
|
93
|
+
expect(dropped[:active]).to eq(rotated[:active].map { |x, y| [x, y + 1] })
|
|
94
|
+
expect(dropped[:stack]).to eq(first[:stack])
|
|
95
|
+
events = 150.times.map do
|
|
96
|
+
game.key(:down)
|
|
97
|
+
game.step[:event]
|
|
98
|
+
end
|
|
99
|
+
expect(events).to include(:lock)
|
|
100
|
+
end
|
|
28
101
|
|
|
102
|
+
it 'turns and thrusts Asteroids only on arrows and fires only on space' do
|
|
103
|
+
game = PWN::Banner::MiniGame.new(name: :asteroids, width: 16, height: 16, seed: 73)
|
|
104
|
+
first = game.step[:state]
|
|
105
|
+
idle = game.step[:state]
|
|
106
|
+
expect(idle[:ship]).to eq(first[:ship])
|
|
107
|
+
expect(idle[:shots]).to be_empty
|
|
108
|
+
game.key(:left)
|
|
109
|
+
left = game.step[:state]
|
|
110
|
+
expect(left[:ship][2]).to be_within(0.001).of((first[:ship][2] - 0.2) % (Math::PI * 2))
|
|
111
|
+
game.key(:right)
|
|
112
|
+
expect(game.step[:state][:ship][2]).to be_within(0.001).of(first[:ship][2])
|
|
113
|
+
game.key(:up)
|
|
114
|
+
moved = game.step[:state]
|
|
115
|
+
expect(moved[:ship].take(2)).not_to eq(first[:ship].take(2))
|
|
116
|
+
expect(moved[:thrust]).to be(true)
|
|
117
|
+
game.key(:down)
|
|
118
|
+
expect(game.step[:state][:thrust]).to be(true)
|
|
119
|
+
game.key(' ')
|
|
120
|
+
expect(game.step[:state][:shots].length).to eq(1)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe PWN::Banner do
|
|
29
125
|
def block_colors(rows)
|
|
30
126
|
rows.flat_map do |row|
|
|
31
127
|
[0, 2].map do |bit|
|
|
@@ -258,6 +258,27 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
258
258
|
foreign&.join
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
it 'leaves a provider blocked in native IO without waiting for that read to return' do
|
|
262
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
263
|
+
allow(PWN::AI::Agent::Loop).to receive(:call_engine) do
|
|
264
|
+
Thread.handle_interrupt(Exception => :never) { sleep 8 }
|
|
265
|
+
end
|
|
266
|
+
keys = "what color is a lemon?\n".chars
|
|
267
|
+
cancelled = false
|
|
268
|
+
launch(keys) do
|
|
269
|
+
if !cancelled && @paint.any? { |row| row.include?('Elapsed') }
|
|
270
|
+
cancelled = true
|
|
271
|
+
"\u0003"
|
|
272
|
+
elsif @paint.any? { |row| row.include?('Request cancelled at a safe boundary') }
|
|
273
|
+
"\u0004"
|
|
274
|
+
else
|
|
275
|
+
Thread.pass
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 3
|
|
280
|
+
end
|
|
281
|
+
|
|
261
282
|
it 'forwards tool input, preserves completed evidence before steering, and locks settings while running' do
|
|
262
283
|
entered = Queue.new
|
|
263
284
|
calls = 0
|
|
@@ -1025,6 +1046,86 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
|
|
|
1025
1046
|
console.draw
|
|
1026
1047
|
expect(painted.map { |row| row[2] }.join).to include('SESSION · active')
|
|
1027
1048
|
expect(screen).to have_received(:setpos).with(a_kind_of(Integer), 2)
|
|
1049
|
+
console.handle("\u0014")
|
|
1050
|
+
painted.clear
|
|
1051
|
+
console.draw
|
|
1052
|
+
expect(painted.map { |row| row[2] }.join).to include(' PLAY ')
|
|
1053
|
+
expect(painted.map { |row| row[2] }.join).not_to include('MISSION CONTROL · active')
|
|
1054
|
+
expect(screen).to have_received(:setpos).with(1, 1)
|
|
1055
|
+
allow(curses).to receive_messages(lines: 24, cols: 80)
|
|
1056
|
+
console.draw
|
|
1057
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
it 'cycles mission, session and visible animation with both shortcuts without changing the draft' do
|
|
1061
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1062
|
+
editor = console.instance_variable_get(:@editor)
|
|
1063
|
+
editor.place('kept draft', 4)
|
|
1064
|
+
console.instance_variable_set(:@header_pane_height, 10)
|
|
1065
|
+
console.banner_frame(8, cells: true)
|
|
1066
|
+
console.handle("\u0014")
|
|
1067
|
+
console.handle("\u0018")
|
|
1068
|
+
expect(console.instance_variable_get(:@focus)).to eq(:animation)
|
|
1069
|
+
console.banner_frame(8, cells: true)
|
|
1070
|
+
game = console.instance_variable_get(:@banner_game)
|
|
1071
|
+
expect(game).to receive(:key).with(:up)
|
|
1072
|
+
expect(game).to receive(:key).with(' ')
|
|
1073
|
+
console.handle(:up)
|
|
1074
|
+
console.handle(' ')
|
|
1075
|
+
expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
|
|
1076
|
+
console.handle("\u0014")
|
|
1077
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1078
|
+
expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
it 'returns to the unchanged automatic animation when focus leaves the game' do
|
|
1082
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1083
|
+
allow(Process).to receive(:clock_gettime).and_return(10.0)
|
|
1084
|
+
automatic = console.banner_frame(8, cells: true)
|
|
1085
|
+
console.instance_variable_set(:@header_pane_height, 10)
|
|
1086
|
+
2.times { console.handle("\u0014") }
|
|
1087
|
+
expect(console.banner_frame(8, cells: true)).not_to eq(automatic)
|
|
1088
|
+
console.handle("\u0018")
|
|
1089
|
+
expect(console.banner_frame(8, cells: true)).to eq(automatic)
|
|
1090
|
+
console.instance_variable_set(:@header_pane_height, nil)
|
|
1091
|
+
2.times { console.handle("\u0014") }
|
|
1092
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
it 'keeps modal, search and cancellation priority above animation controls' do
|
|
1096
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1097
|
+
console.instance_variable_set(:@focus, :animation)
|
|
1098
|
+
editor = console.instance_variable_get(:@editor)
|
|
1099
|
+
editor.place('draft', 2)
|
|
1100
|
+
game = double(key: nil)
|
|
1101
|
+
console.instance_variable_set(:@banner_game, game)
|
|
1102
|
+
console.instance_variable_set(:@details, 0)
|
|
1103
|
+
expect(console).to receive(:handle_details).with(:up)
|
|
1104
|
+
console.handle(:up)
|
|
1105
|
+
console.instance_variable_set(:@details, nil)
|
|
1106
|
+
console.instance_variable_set(:@model_prompt, {})
|
|
1107
|
+
expect(console).to receive(:handle_model_prompt).with(' ')
|
|
1108
|
+
console.handle(' ')
|
|
1109
|
+
console.instance_variable_set(:@model_prompt, nil)
|
|
1110
|
+
allow(console).to receive(:busy?).and_return(true)
|
|
1111
|
+
expect(console).to receive(:cancel)
|
|
1112
|
+
console.handle("\u0003")
|
|
1113
|
+
console.handle("\u0012")
|
|
1114
|
+
expect(editor.search).not_to be_nil
|
|
1115
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1116
|
+
expect(game).not_to have_received(:key)
|
|
1117
|
+
expect([editor.text, editor.cursor]).to eq(['draft', 2])
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
it 'preserves bracketed paste and the mission cursor when leaving animation focus' do
|
|
1121
|
+
console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
|
|
1122
|
+
editor = console.instance_variable_get(:@editor)
|
|
1123
|
+
editor.place('draft', 2)
|
|
1124
|
+
console.instance_variable_set(:@focus, :animation)
|
|
1125
|
+
[:paste_start, ' ', "\n", 'x', :paste_end].each { |key| console.handle(key) }
|
|
1126
|
+
expect(editor.text).to eq("dr \nxaft")
|
|
1127
|
+
expect(editor.cursor).to eq(5)
|
|
1128
|
+
expect(console.instance_variable_get(:@focus)).to eq(:mission)
|
|
1028
1129
|
end
|
|
1029
1130
|
|
|
1030
1131
|
it 'toggles the pane with Ctrl+T and opens swarm with Ctrl+S' do
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -487,9 +487,10 @@
|
|
|
487
487
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.wrap_untrusted_tool Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.wrap_untrusted_tool`: "}]}
|
|
488
488
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_or_read_evidenced? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_or_read_evidenced?`: "}]}
|
|
489
489
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
|
|
490
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::ProviderWindow.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::ProviderWindow.model`: "}]}
|
|
491
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::ProviderWindow.request_model_cancel Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::ProviderWindow.request_model_cancel`: "}]}
|
|
490
492
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.new`: "}]}
|
|
491
493
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.checkpoint Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.checkpoint`: "}]}
|
|
492
|
-
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.model`: "}]}
|
|
493
494
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.submit Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.submit`: "}]}
|
|
494
495
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.with_reader Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.with_reader`: "}]}
|
|
495
496
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering::Restart.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering::Restart.new`: "}]}
|
|
@@ -1734,6 +1735,7 @@
|
|
|
1734
1735
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids`: "}]}
|
|
1735
1736
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids_snapshot`: "}]}
|
|
1736
1737
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_block_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_board`: "}]}
|
|
1738
|
+
{"messages":[{"role":"user","content":"PWN::Banner.mini_block_plan Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_plan`: "}]}
|
|
1737
1739
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_cells Usage"},{"role":"assistant","content":"`PWN::Banner.mini_cells`: Supported Method Parameters\n\nReturn rows of single-column Unicode block cells, with named terminal colors. Each fresh cell has glyph, foreground and background keys; no ANSI or IO. Square means terminal-cell dimensions, not physical pixels. Caller owns borders. Use mini_frame_seconds for cadence; 1800 frames span 90 or 180 seconds. Colored blocks fill the canvas with half-cell pieces; other games cap at 16. PWN::Banner.mini_cells(\n\nname: 'optional - falling_blocks, snake, pong, or asteroids; defaults to falling_blocks',\nframe: 'optional - integer index, defaults to zero and wraps at 1800',\nwidth: 'optional - columns clamped to 0..64 for blocks, 0..16 otherwise; defaults to 16',\nheight: 'optional - rows clamped to 0..64 for blocks, 0..16 otherwise; defaults to 16',\nseed: 'optional - integer local PRNG seed, defaults to 73; retain per session',\nbranding: 'optional - include the legacy wordmark, defaults to true; false reclaims its row'\n\n)\n"}]}
|
|
1738
1740
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_delta Usage"},{"role":"assistant","content":"`PWN::Banner.mini_delta`: "}]}
|
|
1739
1741
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_emit Usage"},{"role":"assistant","content":"`PWN::Banner.mini_emit`: "}]}
|
|
@@ -1745,6 +1747,7 @@
|
|
|
1745
1747
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_landing Usage"},{"role":"assistant","content":"`PWN::Banner.mini_landing`: "}]}
|
|
1746
1748
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_names Usage"},{"role":"assistant","content":"`PWN::Banner.mini_names`: Supported Method Parameters\n\nList animation names; callers may sample once and retain the session choice. PWN::Banner.mini_names\n"}]}
|
|
1747
1749
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_pixel Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pixel`: "}]}
|
|
1750
|
+
{"messages":[{"role":"user","content":"PWN::Banner.mini_play_piece Usage"},{"role":"assistant","content":"`PWN::Banner.mini_play_piece`: "}]}
|
|
1748
1751
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_pong Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong`: "}]}
|
|
1749
1752
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_pong_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong_snapshot`: "}]}
|
|
1750
1753
|
{"messages":[{"role":"user","content":"PWN::Banner.mini_quarter_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_quarter_board`: "}]}
|
|
@@ -1786,6 +1789,13 @@
|
|
|
1786
1789
|
{"messages":[{"role":"user","content":"PWN::Banner::Matrix.get Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.get`: Supported Method Parameters\n\nPWN::Banner::Matrix.get\n"}]}
|
|
1787
1790
|
{"messages":[{"role":"user","content":"PWN::Banner::Matrix.help Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.help`: "}]}
|
|
1788
1791
|
{"messages":[{"role":"user","content":"PWN::Banner::Matrix.you? Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.you?`: Supported Method Parameters\n\nPWN::Banner::Matrix.has_you\n"}]}
|
|
1792
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.new Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.new`: "}]}
|
|
1793
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.<< Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.<<`: "}]}
|
|
1794
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.cells Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.cells`: "}]}
|
|
1795
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.controls Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.controls`: "}]}
|
|
1796
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.key Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.key`: "}]}
|
|
1797
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.length Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.length`: "}]}
|
|
1798
|
+
{"messages":[{"role":"user","content":"PWN::Banner::MiniGame.step Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.step`: "}]}
|
|
1789
1799
|
{"messages":[{"role":"user","content":"PWN::Banner::Ninja.authors Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
|
|
1790
1800
|
{"messages":[{"role":"user","content":"PWN::Banner::Ninja.get Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.get`: Supported Method Parameters\n\nPWN::Banner::Ninja.get\n"}]}
|
|
1791
1801
|
{"messages":[{"role":"user","content":"PWN::Banner::Ninja.help Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.help`: "}]}
|
|
@@ -3339,6 +3349,7 @@
|
|
|
3339
3349
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.footer_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.footer_lines`: "}]}
|
|
3340
3350
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.footer_text Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.footer_text`: "}]}
|
|
3341
3351
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle`: "}]}
|
|
3352
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_animation Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_animation`: "}]}
|
|
3342
3353
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_details Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_details`: "}]}
|
|
3343
3354
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_model_prompt Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_model_prompt`: "}]}
|
|
3344
3355
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_search Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_search`: "}]}
|
|
@@ -3354,6 +3365,7 @@
|
|
|
3354
3365
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_swarm Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_swarm`: "}]}
|
|
3355
3366
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_system_role Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_system_role`: "}]}
|
|
3356
3367
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.operation_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.operation_lines`: "}]}
|
|
3368
|
+
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.playable_banner Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.playable_banner`: "}]}
|
|
3357
3369
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission`: "}]}
|
|
3358
3370
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put`: "}]}
|
|
3359
3371
|
{"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put_spans Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put_spans`: "}]}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.760
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -505,14 +505,14 @@ dependencies:
|
|
|
505
505
|
requirements:
|
|
506
506
|
- - '='
|
|
507
507
|
- !ruby/object:Gem::Version
|
|
508
|
-
version: 1.
|
|
508
|
+
version: 1.7.0
|
|
509
509
|
type: :runtime
|
|
510
510
|
prerelease: false
|
|
511
511
|
version_requirements: !ruby/object:Gem::Requirement
|
|
512
512
|
requirements:
|
|
513
513
|
- - '='
|
|
514
514
|
- !ruby/object:Gem::Version
|
|
515
|
-
version: 1.
|
|
515
|
+
version: 1.7.0
|
|
516
516
|
- !ruby/object:Gem::Dependency
|
|
517
517
|
name: meshtastic
|
|
518
518
|
requirement: !ruby/object:Gem::Requirement
|