lively 0.11.0 → 0.12.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39c0cab1f012ef1adc64dcdb197017f93fe45eed7f81a32b57ce102fc537ee75
4
- data.tar.gz: d5309edbd47e795ecbde9cd34c1d23d8826a6f5ccba4bc79a8b3661077e8ef97
3
+ metadata.gz: ec2c7506f7a9fa9662a11a4d676e524a7cdf58fa43b3adbca8b94e2092a2eba3
4
+ data.tar.gz: d473035b8db2a7092b918c0d993521a9dca1736e20d4ac4a8bd58550a1655e86
5
5
  SHA512:
6
- metadata.gz: 6b672c594ae520e4a44985350ec393fca0074d581e7d2160c2ee03b12265b1aee1d59be3c26ce82038cd64c517673fe094844fd24a74922ecf26af380762c93b
7
- data.tar.gz: '0483d26396b0062b3c40fd141e2827d089f805bc600fc1357f777ecaf220a4c7358dc8dd664dd7f6dcf6bd6c50107f16673bfd2f84564aeaf99214781d4a972e'
6
+ metadata.gz: 417cda1546265cae2b10d075020e05c92573457d6c892a0d594f74bb9253fe1b8a864be75ce7167a5bfa28e17ba26709df05dbf29f3076f6073b846245164e5c
7
+ data.tar.gz: 2ea896d2553f5261202b99e1877fddd668ba954fd1fc2fd8cc4f74faf77effaad6952cce7c39a039bd92cb4d04856e7550330a144cbbf81f57de712359e30bb8
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,77 @@
1
+ # Getting Started
2
+
3
+ This guide will help you get started with Lively, a framework for building real-time applications in Ruby.
4
+
5
+ ## Installation
6
+
7
+ To install Lively, you can use the following command:
8
+
9
+ ```bash
10
+ $ gem install lively
11
+ ```
12
+
13
+ ## Basic Usage
14
+
15
+ Create a new directory for your Lively application:
16
+
17
+ ```bash
18
+ $ mkdir my_lively_app
19
+ $ cd my_lively_app
20
+ ```
21
+
22
+ Then create a `gems.rb` file in your project directory:
23
+
24
+ ```ruby
25
+ source "https://rubygems.org"
26
+ gem "lively"
27
+ ```
28
+
29
+ Next, run `bundle install` to install the Lively gem:
30
+
31
+ ```bash
32
+ $ bundle install
33
+ ```
34
+
35
+ Create an `application.rb` file in your project directory:
36
+
37
+ ```ruby
38
+ #!/usr/bin/env lively
39
+
40
+ class HelloWorldView < Live::View
41
+ def bind(page)
42
+ super
43
+ self.update!
44
+ end
45
+
46
+ def render(builder)
47
+ builder.tag(:p) do
48
+ builder.text("Hello World!")
49
+ end
50
+ end
51
+ end
52
+
53
+ Application = Lively::Application[HelloWorldView]
54
+ ```
55
+
56
+ Now you can run your Lively application:
57
+
58
+ ```bash
59
+ $ chmod +x application.rb
60
+ $ ./application.rb
61
+ ```
62
+
63
+ You should see "Hello World!" displayed in your browser.
64
+
65
+ ## Live Reloading
66
+
67
+ To enable live reloading, add the `io-watch` gem to your `gems.rb` file:
68
+
69
+ ```ruby
70
+ gem "io-watch"
71
+ ```
72
+
73
+ Then run:
74
+
75
+ ```bash
76
+ $ io-watch . -- ./application.rb
77
+ ```
@@ -0,0 +1,16 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A simple client-server SPA framework.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/lively/
7
+ source_code_uri: https://github.com/socketry/lively.git
8
+ files:
9
+ - path: getting-started.md
10
+ title: Getting Started
11
+ description: This guide will help you get started with Lively, a framework for building
12
+ real-time applications in Ruby.
13
+ - path: worms-tutorial.md
14
+ title: Building a Worms Game with Lively
15
+ description: This tutorial will guide you through creating a Worms-style game using
16
+ Lively, a Ruby framework for building real-time applications.
@@ -0,0 +1,842 @@
1
+ # Building a Worms Game with Lively
2
+
3
+ This tutorial will guide you through creating a Worms-style game using Lively, a Ruby framework for building real-time applications.
4
+
5
+ We'll build the game step by step, starting with simple concepts and gradually adding complexity.
6
+
7
+ ## What You'll Build
8
+
9
+ By the end of this tutorial, you'll have created:
10
+
11
+ - A grid-based game board that you can see in your browser.
12
+ - A simple worm that moves automatically.
13
+ - Manual control with keyboard input.
14
+ - Fruit collection mechanics.
15
+ - Real-time updates using WebSockets.
16
+
17
+ ## Prerequisites
18
+
19
+ - Basic knowledge of Ruby programming.
20
+ - Understanding of classes and objects.
21
+ - Familiarity with HTML/CSS basics.
22
+ - Lively framework installed (follow the getting started guide if needed).
23
+
24
+ ## Tutorial Approach
25
+
26
+ We'll build this game in stages:
27
+
28
+ 1. **Static Board**: First, we'll create a simple grid that displays in the browser.
29
+ 2. **Dynamic Board**: Add the ability to change cells and see updates.
30
+ 3. **Simple Worm**: Create a worm that moves automatically.
31
+ 4. **User Control**: Add keyboard input to control the worm.
32
+ 5. **Game Mechanics**: Add fruit, collision detection, and game rules.
33
+
34
+ ## Step 1: Setting Up the Project Structure
35
+
36
+ First, create a new directory for your Worms game:
37
+
38
+ ```bash
39
+ $ mkdir my_worms_game
40
+ $ cd my_worms_game
41
+ ```
42
+
43
+ Create the main application file:
44
+
45
+ ```bash
46
+ $ touch application.rb
47
+ ```
48
+
49
+ Create the CSS directory and file:
50
+
51
+ ```bash
52
+ $ mkdir -p public/_static
53
+ $ touch public/_static/index.css
54
+ ```
55
+
56
+ ## Step 2: Creating a Static Board (Your First View)
57
+
58
+ Let's start with the simplest possible thing: a grid that shows up in your browser. This will help you understand how Lively works.
59
+
60
+ Create your first file called `static_view.rb`:
61
+
62
+ ```ruby
63
+ #!/usr/bin/env lively
64
+ # frozen_string_literal: true
65
+
66
+ # Reference-style static board: fruit as string, worm as colored segment (hash).
67
+ class StaticBoard
68
+ def initialize(width = 5, height = 5)
69
+ @width = width
70
+ @height = height
71
+
72
+ # Use an Array of Arrays to store a grid:
73
+ @grid = Array.new(@height) {Array.new(@width)}
74
+
75
+ # Place a fruit:
76
+ @grid[1][1] = "🍎"
77
+ # Place a worm segment (hash with color):
78
+ @grid[2][3] = {color: "hsl(120, 80%, 50%)", count: 3}
79
+ # Place another fruit:
80
+ @grid[3][2] = "🍌"
81
+ end
82
+
83
+ attr_reader :grid, :width, :height
84
+ end
85
+
86
+ class StaticView < Live::View
87
+ def initialize(...)
88
+ super
89
+
90
+ @board = StaticBoard.new
91
+ end
92
+
93
+ # Render the HTML grid:
94
+ def render(builder)
95
+ builder.tag("h1") {builder.text("My First Lively Game!")}
96
+ builder.tag("table") do
97
+ @board.grid.each do |row|
98
+ builder.tag("tr") do
99
+ row.each do |cell|
100
+ if cell.is_a?(Hash)
101
+ builder.tag("td", style: "background-color: #{cell[:color]};")
102
+ elsif cell.is_a?(String)
103
+ builder.tag("td") {builder.text(cell)}
104
+ else
105
+ builder.tag("td")
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ Application = Lively::Application[StaticView]
115
+ ```
116
+
117
+ Now add some basic CSS to `public/_static/index.css`:
118
+
119
+ ```css
120
+ body {
121
+ display: flex;
122
+ flex-direction: column;
123
+ align-items: center;
124
+ margin: 20px;
125
+ font-family: Arial, sans-serif;
126
+ }
127
+
128
+ table {
129
+ border-collapse: collapse;
130
+ margin: 20px;
131
+ }
132
+
133
+ td {
134
+ width: 40px;
135
+ height: 40px;
136
+ border: 1px solid #ccc;
137
+ text-align: center;
138
+ vertical-align: middle;
139
+ font-size: 20px;
140
+ }
141
+ ```
142
+
143
+ **Test it now**: Run `lively static_view.rb` and open your browser. You should see a 5x5 grid with a few emoji scattered around!
144
+
145
+ **What's happening here:**
146
+ - `SimpleBoard` creates a 2D array representing our game grid
147
+ - `StaticView` renders this grid as an HTML table
148
+ - The CSS makes it look like a proper game board
149
+ - Everything is static - no movement or interaction yet
150
+
151
+ ## Step 3: Making the Board Interactive
152
+
153
+ Now let's make the board update when we click on it. This introduces the key concept of real-time updates in Lively.
154
+
155
+ Create a new file called `interactive_view.rb`:
156
+
157
+ ```ruby
158
+ #!/usr/bin/env lively
159
+ # frozen_string_literal: true
160
+
161
+ # Reference-style interactive board: toggles between fruit and colored segment
162
+ class InteractiveBoard
163
+ def initialize(width = 5, height = 5)
164
+ @width = width
165
+ @height = height
166
+ @grid = Array.new(@height) {Array.new(@width)}
167
+ # Put a fruit in the center:
168
+ @grid[2][2] = "🍎"
169
+ end
170
+
171
+ attr_reader :grid, :width, :height
172
+
173
+ # Set a worm segment at the specified coordinates.
174
+ def set_segment(y, x)
175
+ @grid[y][x] = {color: "hsl(120, 80%, 50%)", count: 1}
176
+ end
177
+
178
+ # Set a fruit at the specified coordinates.
179
+ def set_fruit(y, x)
180
+ @grid[y][x] = "🍎"
181
+ end
182
+
183
+ # Clear a cell at the specified coordinates.
184
+ def clear_cell(y, x)
185
+ @grid[y][x] = nil
186
+ end
187
+
188
+ # Get the cell at the specified coordinates.
189
+ def get_cell(y, x)
190
+ @grid[y][x]
191
+ end
192
+ end
193
+
194
+ class InteractiveView < Live::View
195
+ def initialize(...)
196
+ super
197
+ @board = InteractiveBoard.new
198
+ end
199
+
200
+ # Handle input from the user.
201
+ def handle(event)
202
+ Console.info(self, "Received event:", event)
203
+
204
+ # Handle click events:
205
+ if event[:type] == "click"
206
+ # Get the x, y coordinates of the cell that was clicked:
207
+ y = event[:detail][:y].to_i
208
+ x = event[:detail][:x].to_i
209
+
210
+ # Get the current value of the cell:
211
+ cell = @board.get_cell(y, x)
212
+
213
+ # Toggle: empty → fruit → segment → empty
214
+ if cell.nil?
215
+ @board.set_fruit(y, x)
216
+ elsif cell.is_a?(String)
217
+ @board.set_segment(y, x)
218
+ else
219
+ @board.clear_cell(y, x)
220
+ end
221
+ self.update!
222
+ end
223
+ end
224
+
225
+ # Render the HTML grid, including event forwarding.
226
+ def render(builder)
227
+ builder.tag("h1") {builder.text("Interactive Board - Click the cells!")}
228
+ builder.tag("table") do
229
+ @board.grid.each_with_index do |row, y|
230
+ builder.tag("tr") do
231
+ row.each_with_index do |cell, x|
232
+ if cell.is_a?(Hash)
233
+ # lively.forwardEvent sends the event from the browser to the server, invoking the handle method above. Note that we include the x and y coordinates as extra details.
234
+ builder.tag("td", onclick: "live.forwardEvent('#{@id}', event, {y: #{y}, x: #{x}});", style: "cursor: pointer; background-color: #{cell[:color]};")
235
+ elsif cell.is_a?(String)
236
+ builder.tag("td", onclick: "live.forwardEvent('#{@id}', event, {y: #{y}, x: #{x}});", style: "cursor: pointer;") {builder.text(cell)}
237
+ else
238
+ builder.tag("td", onclick: "live.forwardEvent('#{@id}', event, {y: #{y}, x: #{x}});", style: "cursor: pointer;")
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ builder.tag("p") {builder.text("Click any cell to cycle: empty → fruit → worm segment → empty.")}
245
+ end
246
+ end
247
+
248
+ Application = Lively::Application[InteractiveView]
249
+ ```
250
+
251
+ **Test it now**: Run `lively interactive_view.rb` and click on the grid cells. You should see stars appear and disappear!
252
+
253
+ **Key concepts you just learned:**
254
+ - `handle(event)` receives user interactions from the browser
255
+ - `self.update!` tells Lively to re-render the page with new data
256
+ - JavaScript `onclick` events can send data back to Ruby
257
+ - The board can be modified and the changes appear instantly
258
+
259
+ **Try this**: Click around the grid and watch the real-time updates. This is the foundation of all Lively applications!
260
+
261
+ ## Step 4: Adding a Simple Moving Worm
262
+
263
+ Now let's add something that moves automatically. This introduces the concept of background tasks and animation.
264
+
265
+ Create a new file called `moving_worm.rb`:
266
+
267
+ ```ruby
268
+ #!/usr/bin/env lively
269
+ # frozen_string_literal: true
270
+
271
+ # Reference-style: worm as colored segment (hash), leaves a trail, bounces off walls.
272
+ class Board
273
+ def initialize(width = 8, height = 8)
274
+ @width = width
275
+ @height = height
276
+ @grid = Array.new(@height) {Array.new(@width)}
277
+ end
278
+
279
+ attr_reader :grid, :width, :height
280
+
281
+ def set_segment(y, x, color, count)
282
+ @grid[y][x] = {color: color, count: count}
283
+ end
284
+
285
+ def clear_segment(y, x)
286
+ if @grid[y][x].is_a?(Hash)
287
+ @grid[y][x] = nil
288
+ end
289
+ end
290
+
291
+ # For all values in the grid that are integers, decrement them by 1. If they become 0, clear the segment.
292
+ def decrement_segments
293
+ @grid.each do |row|
294
+ row.map! do |cell|
295
+ if cell.is_a?(Hash)
296
+ cell[:count] -= 1
297
+ cell[:count] > 0 ? cell : nil
298
+ else
299
+ cell
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+ class SimpleWorm
307
+ def initialize(board, start_y, start_x, color = "hsl(120, 80%, 50%)")
308
+ @board = board
309
+ @y = start_y
310
+ @x = start_x
311
+ @direction = :right
312
+ @color = color
313
+ @length = 3
314
+ @board.set_segment(@y, @x, @color, @length)
315
+ end
316
+
317
+ attr_reader :y, :x, :direction
318
+
319
+ def move
320
+ # Calculate new position based on the movement direction:
321
+ new_y, new_x = @y, @x
322
+ case @direction
323
+ when :right
324
+ new_x += 1
325
+ when :left
326
+ new_x -= 1
327
+ when :up
328
+ new_y -= 1
329
+ when :down
330
+ new_y += 1
331
+ end
332
+
333
+ # Bounce off walls by changing direction:
334
+ if new_y < 0 || new_y >= @board.height || new_x < 0 || new_x >= @board.width
335
+ case @direction
336
+ when :right
337
+ @direction = :down
338
+ when :left
339
+ @direction = :up
340
+ when :up
341
+ @direction = :right
342
+ when :down
343
+ @direction = :left
344
+ end
345
+
346
+ # Don't move this tick
347
+ return
348
+ end
349
+
350
+ # Otherwise, update the worm's position:
351
+ @y, @x = new_y, new_x
352
+ @board.set_segment(@y, @x, @color, @length)
353
+ end
354
+ end
355
+
356
+ class MovingWormView < Live::View
357
+ def initialize(...)
358
+ super
359
+ @board = Board.new
360
+ @worm = SimpleWorm.new(@board, 4, 4)
361
+
362
+ # We will store the task responsible for updating the worm's position ont he server:
363
+ @animation = nil
364
+ end
365
+
366
+ # When the browser connects to the server, this method is invoked, and we set up the animation loop.
367
+ def bind(page)
368
+ super
369
+
370
+ @animation ||= Async do
371
+ # The animation loop repeats 2 times per second, updating the board, then moving the worm.
372
+ loop do
373
+ sleep(0.5)
374
+ @board.decrement_segments
375
+ @worm.move
376
+
377
+ # Regenerate the HTML and send it to the browser:
378
+ self.update!
379
+ end
380
+ end
381
+ end
382
+
383
+ # When the browser disconnects from the server, this method is invoked, and we stop the animation loop.
384
+ def close
385
+ if animation = @animation
386
+ @animation = nil
387
+ animation.stop
388
+ end
389
+
390
+ super
391
+ end
392
+
393
+ # Render the HTML grid, including event forwarding.
394
+ def render(builder)
395
+ builder.tag("h1") {builder.text("Automatic Moving Worm")}
396
+ builder.tag("table") do
397
+ @board.grid.each_with_index do |row, y|
398
+ builder.tag("tr") do
399
+ row.each_with_index do |cell, x|
400
+ if cell.is_a?(Hash)
401
+ builder.tag("td", style: "background-color: #{cell[:color]};")
402
+ else
403
+ builder.tag("td")
404
+ end
405
+ end
406
+ end
407
+ end
408
+ end
409
+ builder.tag("p") {builder.text("Watch the colored worm bounce around and leave a trail!")}
410
+ builder.tag("p") {builder.text("Current position: (#{@worm.y}, #{@worm.x})")}
411
+ end
412
+ end
413
+
414
+ Application = Lively::Application[MovingWormView]
415
+ ```
416
+
417
+ **Test it now**: Run `lively moving_worm.rb` and watch the worm move around the board automatically!
418
+
419
+ ## Step 5: Adding Keyboard Control
420
+
421
+ Now let's make the worm respond to your keyboard input. This is where it becomes a real game!
422
+
423
+
424
+ Create a new file called `controllable_worm.rb`:
425
+
426
+ ```ruby
427
+ #!/usr/bin/env lively
428
+ # frozen_string_literal: true
429
+
430
+ # Reference-style: worm as colored segment (hash), keyboard control, colored trail.
431
+ class Board
432
+ def initialize(width = 10, height = 10)
433
+ @width = width
434
+ @height = height
435
+ @grid = Array.new(@height) {Array.new(@width)}
436
+ end
437
+
438
+ attr_reader :grid, :width, :height
439
+
440
+ def set_segment(y, x, color, count)
441
+ @grid[y][x] = {color: color, count: count}
442
+ end
443
+
444
+ def clear_segment(y, x)
445
+ if @grid[y][x].is_a?(Hash)
446
+ @grid[y][x] = nil
447
+ end
448
+ end
449
+
450
+ def decrement_segments
451
+ @grid.each do |row|
452
+ row.map! do |cell|
453
+ if cell.is_a?(Hash)
454
+ cell[:count] -= 1
455
+ cell[:count] > 0 ? cell : nil
456
+ else
457
+ cell
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
463
+
464
+ class ControllableWorm
465
+ attr_reader :y, :x, :direction
466
+ attr_writer :direction
467
+
468
+ def initialize(board, start_y, start_x, color = "hsl(120, 80%, 50%)")
469
+ @board = board
470
+ @y = start_y
471
+ @x = start_x
472
+ @direction = :right
473
+ @color = color
474
+ @length = 3
475
+ @board.set_segment(@y, @x, @color, @length)
476
+ end
477
+
478
+ def move
479
+ # Calculate new position:
480
+ new_y, new_x = @y, @x
481
+ case @direction
482
+ when :right
483
+ new_x += 1
484
+ when :left
485
+ new_x -= 1
486
+ when :up
487
+ new_y -= 1
488
+ when :down
489
+ new_y += 1
490
+ end
491
+
492
+ # Stay in bounds:
493
+ if new_y < 0 || new_y >= @board.height || new_x < 0 || new_x >= @board.width
494
+ return
495
+ end
496
+
497
+ @y, @x = new_y, new_x
498
+ @board.set_segment(@y, @x, @color, @length)
499
+ end
500
+ end
501
+
502
+ class ControllableView < Live::View
503
+ def initialize(...)
504
+ super
505
+ @board = Board.new
506
+ @worm = ControllableWorm.new(@board, 5, 5)
507
+ @movement = nil
508
+ end
509
+
510
+ def bind(page)
511
+ super
512
+
513
+ @movement ||= Async do
514
+ loop do
515
+ sleep(0.3)
516
+ @board.decrement_segments
517
+ @worm.move
518
+ self.update!
519
+ end
520
+ end
521
+ end
522
+
523
+ def close
524
+ if movement = @movement
525
+ @movement = nil
526
+ movement.stop
527
+ end
528
+
529
+ super
530
+ end
531
+
532
+ def handle(event)
533
+ Console.info(self, "Event:", event)
534
+
535
+ if event[:type] == "keypress"
536
+ key = event[:detail][:key]
537
+ case key
538
+ when "w"
539
+ @worm.direction = :up
540
+ when "s"
541
+ @worm.direction = :down
542
+ when "a"
543
+ @worm.direction = :left
544
+ when "d"
545
+ @worm.direction = :right
546
+ end
547
+ end
548
+ end
549
+
550
+ def render(builder)
551
+ builder.tag("h1") {builder.text("Controllable Worm - Use WASD!")}
552
+ builder.tag("table", tabindex: 0, autofocus: true, onkeypress: "live.forwardEvent('#{@id}', event, {key: event.key});") do
553
+ @board.grid.each_with_index do |row, y|
554
+ builder.tag("tr") do
555
+ row.each_with_index do |cell, x|
556
+ if cell.is_a?(Hash)
557
+ builder.tag("td", style: "background-color: #{cell[:color]};")
558
+ else
559
+ builder.tag("td")
560
+ end
561
+ end
562
+ end
563
+ end
564
+ end
565
+
566
+ # Log extra information about the game:
567
+ builder.tag("div") do
568
+ builder.tag("p") {builder.text("Controls: W (up), A (left), S (down), D (right)")}
569
+ builder.tag("p") {builder.text("Current direction: #{@worm.direction}")}
570
+ builder.tag("p") {builder.text("Position: (#{@worm.y}, #{@worm.x})")}
571
+ builder.tag("p") {builder.text("Click on the game board first, then use WASD keys!")}
572
+ end
573
+ end
574
+ end
575
+
576
+ Application = Lively::Application[ControllableView]
577
+ ```
578
+
579
+ **Test it now**:
580
+ 1. Run `lively controllable_worm.rb`
581
+ 2. Click on the game board to focus it
582
+ 3. Use W, A, S, D keys to control the worm!
583
+
584
+ **What you just learned:**
585
+ - How to capture keyboard events with `onkeypress`
586
+ - Using `tabindex` and `autofocus` to make elements keyboard-focusable
587
+ - Separating input handling from movement logic
588
+ - Real-time control of game objects
589
+
590
+ **Try this**:
591
+ - Change direction while the worm is moving
592
+ - Try to "trap" the worm in a corner
593
+ - Experiment with different movement speeds
594
+
595
+ ## Step 6: The Complete Game
596
+
597
+ Let's put it all together to create the full Worms game! This includes trails, fruit collection, growing mechanics, and collision detection.
598
+
599
+
600
+ Create a new file called `complete_game.rb`:
601
+
602
+ ```ruby
603
+ #!/usr/bin/env lively
604
+ # frozen_string_literal: true
605
+
606
+ # Reference-style board and rendering
607
+ class Board
608
+ FRUITS = ["🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🍈", "🍒"]
609
+
610
+ def initialize(width = 15, height = 15)
611
+ @width = width
612
+ @height = height
613
+ @grid = Array.new(@height) {Array.new(@width)}
614
+ @fruit_count = 0
615
+ reset!
616
+ end
617
+
618
+ attr_reader :grid, :width, :height
619
+
620
+ def add_fruit!
621
+ 10.times do
622
+ y = rand(@height)
623
+ x = rand(@width)
624
+ if @grid[y][x].nil?
625
+ @grid[y][x] = FRUITS.sample
626
+ @fruit_count += 1
627
+ return [y, x]
628
+ end
629
+ end
630
+ nil
631
+ end
632
+
633
+ def remove_fruit!(y, x)
634
+ if @grid[y][x].is_a?(String)
635
+ @grid[y][x] = nil
636
+ @fruit_count -= 1
637
+ end
638
+ end
639
+
640
+ def reset!
641
+ @grid.each {|row| row.fill(nil)}
642
+ @fruit_count = 0
643
+ add_fruit!
644
+ end
645
+
646
+ def set_segment(y, x, color, count)
647
+ @grid[y][x] = {color: color, count: count}
648
+ end
649
+
650
+ def clear_segment(y, x)
651
+ if @grid[y][x].is_a?(Hash)
652
+ @grid[y][x] = nil
653
+ end
654
+ end
655
+
656
+ def decrement_segments
657
+ @grid.each do |row|
658
+ row.map! do |cell|
659
+ if cell.is_a?(Hash)
660
+ cell[:count] -= 1
661
+ cell[:count] > 0 ? cell : nil
662
+ else
663
+ cell
664
+ end
665
+ end
666
+ end
667
+ end
668
+ end
669
+
670
+ class Player
671
+ attr_reader :head, :count, :color, :direction, :score
672
+ attr_writer :direction
673
+
674
+ def initialize(board, start_y, start_x, color)
675
+ @board = board
676
+ @head = [start_y, start_x]
677
+ @count = 3
678
+ @direction = :right
679
+ @color = color
680
+ @score = 0
681
+ @board.set_segment(@head[0], @head[1], @color, @count)
682
+ end
683
+
684
+ def move
685
+ # Calculate new head position:
686
+ y, x = @head
687
+ case @direction
688
+ when :up
689
+ y -= 1
690
+ when :down
691
+ y += 1
692
+ when :left
693
+ x -= 1
694
+ when :right
695
+ x += 1
696
+ end
697
+
698
+ # Check for wall collision:
699
+ if y < 0 || y >= @board.height || x < 0 || x >= @board.width
700
+ reset!
701
+ return :wall
702
+ end
703
+
704
+ cell = @board.grid[y][x]
705
+ case cell
706
+ when String # Fruit
707
+ @score += 10
708
+ @count += 2
709
+ @board.remove_fruit!(y, x)
710
+ @board.add_fruit!
711
+ when Hash # Self collision
712
+ reset!
713
+ return :self
714
+ end
715
+
716
+ @head = [y, x]
717
+ @board.set_segment(y, x, @color, @count)
718
+ nil
719
+ end
720
+
721
+ def reset!
722
+ # Remove all segments of this color
723
+ @board.grid.each_with_index do |row, y|
724
+ row.each_with_index do |cell, x|
725
+ if cell.is_a?(Hash) && cell[:color] == @color
726
+ @board.clear_segment(y, x)
727
+ end
728
+ end
729
+ end
730
+ @head = [@board.height/2, @board.width/2]
731
+ @count = 3
732
+ @direction = :right
733
+ @score = 0
734
+ @board.set_segment(@head[0], @head[1], @color, @count)
735
+ end
736
+ end
737
+
738
+ class WormsGameView < Live::View
739
+ def initialize(...)
740
+ super
741
+ @board = Board.new
742
+ @player = Player.new(@board, @board.height/2, @board.width/2, "hsl(120, 80%, 50%)")
743
+ end
744
+
745
+ def bind(page)
746
+ super
747
+
748
+ @game ||= Async do
749
+ loop do
750
+ sleep(0.18)
751
+ @board.decrement_segments
752
+ @player.move
753
+ self.update!
754
+ end
755
+ end
756
+ end
757
+
758
+ def close
759
+ @game&.stop
760
+ super
761
+ end
762
+
763
+ def handle(event)
764
+ if event[:type] == "keypress"
765
+ key = event[:detail][:key]
766
+ case key
767
+ when "w"
768
+ @player.direction = :up unless @player.direction == :down
769
+ when "s"
770
+ @player.direction = :down unless @player.direction == :up
771
+ when "a"
772
+ @player.direction = :left unless @player.direction == :right
773
+ when "d"
774
+ @player.direction = :right unless @player.direction == :left
775
+ end
776
+ end
777
+ end
778
+
779
+ def render(builder)
780
+ builder.tag("h1") {builder.text("Worms Game (Reference-style)")}
781
+ builder.tag("div") do
782
+ builder.tag("p") {builder.text("Score: #{@player.score}")}
783
+ builder.tag("p") {builder.text("Length: #{@player.count}")}
784
+ builder.tag("p") {builder.text("Direction: #{@player.direction}")}
785
+ end
786
+ builder.tag("table", tabindex: 0, autofocus: true, onkeypress: "live.forwardEvent('#{@id}', event, {key: event.key});") do
787
+ @board.grid.each do |row|
788
+ builder.tag("tr") do
789
+ row.each do |cell|
790
+ if cell.is_a?(Hash)
791
+ builder.tag("td", style: "background-color: #{cell[:color]};")
792
+ elsif cell.is_a?(String)
793
+ builder.tag("td") {builder.text(cell)}
794
+ else
795
+ builder.tag("td")
796
+ end
797
+ end
798
+ end
799
+ end
800
+ end
801
+ builder.tag("div") do
802
+ builder.text("Controls: W/A/S/D to move. Eat fruit to grow. Don't hit yourself or the wall!")
803
+ end
804
+ end
805
+ end
806
+
807
+ Application = Lively::Application[WormsGameView]
808
+ ```
809
+
810
+ **Test it now**: Run `lively complete_game.rb` and enjoy the full game!
811
+
812
+ **What you've accomplished:**
813
+ You now have a fully functional Worms game that demonstrates all the key Lively concepts:
814
+ - Real-time web interfaces with growing trails
815
+ - Complete collision detection and game mechanics
816
+ - Score tracking and game over/restart functionality
817
+ - Polished user interface with CSS styling
818
+
819
+ **Congratulations!** You've built a complete game from the ground up, learning each concept step by step.
820
+
821
+ ## Next Steps and Customization Ideas
822
+
823
+ Now that you understand how Lively works, try these enhancements:
824
+
825
+ 1. **Adjust Game Speed**: Change the sleep time in the game loop.
826
+ 2. **Bigger Board**: Modify the width and height parameters.
827
+ 3. **Different Fruits**: Add new emoji to the `FRUITS` array.
828
+ 4. **Power-ups**: Create special fruits with different effects.
829
+ 5. **High Scores**: Store and display the best scores.
830
+ 6. **Sound Effects**: Add audio feedback for actions.
831
+ 7. **Multiplayer**: Create separate game instances for different players.
832
+ 8. **Touch Controls**: Add swipe gestures for mobile devices.
833
+
834
+ ## Key Lively Concepts
835
+
836
+ This tutorial demonstrated the core concepts you need for any Lively application:
837
+
838
+ - **Views**: Ruby classes that generate HTML content.
839
+ - **Event Handling**: Processing user interactions from the browser.
840
+ - **Real-time Updates**: Using `self.update!` to refresh content.
841
+ - **Background Tasks**: Using `Async` for continuous processes.
842
+ - **Resource Management**: Cleaning up with the `close` method.
@@ -13,9 +13,9 @@ module Lively
13
13
  module Application
14
14
  include Falcon::Environment::Server
15
15
 
16
- # def url
17
- # "http://localhost:9292"
18
- # end
16
+ def url
17
+ "http://localhost:9292"
18
+ end
19
19
 
20
20
  def count
21
21
  1
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  module Lively
7
- VERSION = "0.11.0"
7
+ VERSION = "0.12.0"
8
8
  end
data/readme.md CHANGED
@@ -6,15 +6,11 @@ A Ruby framework for building interactive web applications for creative coding.
6
6
 
7
7
  ## Usage
8
8
 
9
- See the various examples in the `examples/` directory.
9
+ Please see the [project documentation](https://socketry.github.io/lively/) for more details.
10
10
 
11
- ### Live Coding
11
+ - [Getting Started](https://socketry.github.io/lively/guides/getting-started/index) - This guide will help you get started with Lively, a framework for building real-time applications in Ruby.
12
12
 
13
- You can use `entr` to reload the server when files change:
14
-
15
- ``` bash
16
- $ ls **/*.rb | entr -r bundle exec ./application.rb
17
- ```
13
+ - [Building a Worms Game with Lively](https://socketry.github.io/lively/guides/worms-tutorial/index) - This tutorial will guide you through creating a Worms-style game using Lively, a Ruby framework for building real-time applications. We'll build the game step by step, starting with simple concepts and gradually adding complexity.
18
14
 
19
15
  ## Contributing
20
16
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lively
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -36,7 +36,7 @@ cert_chain:
36
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
38
  -----END CERTIFICATE-----
39
- date: 2025-05-08 00:00:00.000000000 Z
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: falcon
@@ -86,6 +86,9 @@ extensions: []
86
86
  extra_rdoc_files: []
87
87
  files:
88
88
  - bin/lively
89
+ - context/getting-started.md
90
+ - context/index.yaml
91
+ - context/worms-tutorial.md
89
92
  - lib/lively.rb
90
93
  - lib/lively/application.rb
91
94
  - lib/lively/assets.rb
@@ -129,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
132
  - !ruby/object:Gem::Version
130
133
  version: '0'
131
134
  requirements: []
132
- rubygems_version: 3.6.2
135
+ rubygems_version: 3.6.9
133
136
  specification_version: 4
134
137
  summary: A simple client-server SPA framework.
135
138
  test_files: []
metadata.gz.sig CHANGED
Binary file