lively 0.10.1 → 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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bin/lively +3 -2
- data/context/getting-started.md +77 -0
- data/context/index.yaml +16 -0
- data/context/worms-tutorial.md +842 -0
- data/lib/lively/application.rb +6 -6
- data/lib/lively/assets.rb +8 -7
- data/lib/lively/environment/application.rb +8 -8
- data/lib/lively/hello_world.rb +5 -0
- data/lib/lively/pages/index.rb +1 -1
- data/lib/lively/version.rb +1 -1
- data/lib/lively.rb +4 -4
- data/license.md +1 -1
- data/readme.md +6 -10
- data.tar.gz.sig +0 -0
- metadata +9 -11
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec2c7506f7a9fa9662a11a4d676e524a7cdf58fa43b3adbca8b94e2092a2eba3
|
4
|
+
data.tar.gz: d473035b8db2a7092b918c0d993521a9dca1736e20d4ac4a8bd58550a1655e86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 417cda1546265cae2b10d075020e05c92573457d6c892a0d594f74bb9253fe1b8a864be75ce7167a5bfa28e17ba26709df05dbf29f3076f6073b846245164e5c
|
7
|
+
data.tar.gz: 2ea896d2553f5261202b99e1877fddd668ba954fd1fc2fd8cc4f74faf77effaad6952cce7c39a039bd92cb4d04856e7550330a144cbbf81f57de712359e30bb8
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/bin/lively
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require_relative
|
4
|
+
require "async/service"
|
5
|
+
require_relative "../lib/lively/environment/application"
|
5
6
|
|
6
7
|
ARGV.each do |path|
|
7
8
|
require(path)
|
@@ -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
|
+
```
|
data/context/index.yaml
ADDED
@@ -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.
|
data/lib/lively/application.rb
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2021-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
6
|
+
require "live"
|
7
|
+
require "protocol/http/middleware"
|
8
|
+
require "async/websocket/adapters/http"
|
9
9
|
|
10
|
-
require_relative
|
11
|
-
require_relative
|
10
|
+
require_relative "pages/index"
|
11
|
+
require_relative "hello_world"
|
12
12
|
|
13
13
|
module Lively
|
14
14
|
class Application < Protocol::HTTP::Middleware
|
@@ -57,7 +57,7 @@ module Lively
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def call(request)
|
60
|
-
if request.path ==
|
60
|
+
if request.path == "/live"
|
61
61
|
return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
|
62
62
|
else
|
63
63
|
return handle(request)
|
data/lib/lively/assets.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2021-
|
4
|
+
# Copyright, 2021-2025, by Samuel Williams.
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
6
|
+
require "protocol/http/middleware"
|
7
|
+
require "protocol/http/body/file"
|
8
8
|
|
9
9
|
module Lively
|
10
10
|
class Assets < Protocol::HTTP::Middleware
|
11
|
-
DEFAULT_CACHE_CONTROL =
|
11
|
+
DEFAULT_CACHE_CONTROL = "no-store, no-cache, must-revalidate, max-age=0"
|
12
12
|
|
13
13
|
DEFAULT_CONTENT_TYPES = {
|
14
14
|
".html" => "text/html",
|
@@ -18,9 +18,10 @@ module Lively
|
|
18
18
|
".jpeg" => "image/jpeg",
|
19
19
|
".gif" => "image/gif",
|
20
20
|
".mp3" => "audio/mpeg",
|
21
|
+
".wav" => "audio/wav",
|
21
22
|
}
|
22
23
|
|
23
|
-
PUBLIC_ROOT = File.expand_path(
|
24
|
+
PUBLIC_ROOT = File.expand_path("../../public", __dir__)
|
24
25
|
|
25
26
|
def initialize(delegate, root: PUBLIC_ROOT, content_types: DEFAULT_CONTENT_TYPES, cache_control: DEFAULT_CACHE_CONTROL)
|
26
27
|
super(delegate)
|
@@ -43,8 +44,8 @@ module Lively
|
|
43
44
|
|
44
45
|
def response_for(path, content_type)
|
45
46
|
headers = [
|
46
|
-
[
|
47
|
-
[
|
47
|
+
["content-type", content_type],
|
48
|
+
["cache-control", @cache_control],
|
48
49
|
]
|
49
50
|
|
50
51
|
return Protocol::HTTP::Response[200, headers, Protocol::HTTP::Body::File.open(path)]
|
@@ -3,19 +3,19 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2021-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
6
|
+
require_relative "../application"
|
7
|
+
require_relative "../assets"
|
8
8
|
|
9
|
-
require
|
9
|
+
require "falcon/environment/server"
|
10
10
|
|
11
11
|
module Lively
|
12
12
|
module Environment
|
13
13
|
module Application
|
14
14
|
include Falcon::Environment::Server
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
def url
|
17
|
+
"http://localhost:9292"
|
18
|
+
end
|
19
19
|
|
20
20
|
def count
|
21
21
|
1
|
@@ -32,8 +32,8 @@ module Lively
|
|
32
32
|
|
33
33
|
def middleware
|
34
34
|
::Protocol::HTTP::Middleware.build do |builder|
|
35
|
-
builder.use Lively::Assets, root: File.expand_path(
|
36
|
-
builder.use Lively::Assets, root: File.expand_path(
|
35
|
+
builder.use Lively::Assets, root: File.expand_path("public", self.root)
|
36
|
+
builder.use Lively::Assets, root: File.expand_path("../../../public", __dir__)
|
37
37
|
builder.use self.application
|
38
38
|
end
|
39
39
|
end
|
data/lib/lively/hello_world.rb
CHANGED
data/lib/lively/pages/index.rb
CHANGED
data/lib/lively/version.rb
CHANGED
data/lib/lively.rb
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2021-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "lively/version"
|
7
7
|
|
8
|
-
require_relative
|
9
|
-
require_relative
|
8
|
+
require_relative "lively/assets"
|
9
|
+
require_relative "lively/application"
|
10
10
|
|
11
|
-
require_relative
|
11
|
+
require_relative "lively/environment/application"
|
data/license.md
CHANGED
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
|
-
|
9
|
+
Please see the [project documentation](https://socketry.github.io/lively/) for more details.
|
10
10
|
|
11
|
-
|
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
|
-
|
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
|
|
@@ -28,11 +24,11 @@ We welcome contributions to this project.
|
|
28
24
|
|
29
25
|
### Developer Certificate of Origin
|
30
26
|
|
31
|
-
|
27
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
32
28
|
|
33
|
-
###
|
29
|
+
### Community Guidelines
|
34
30
|
|
35
|
-
This project is
|
31
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
36
32
|
|
37
33
|
## See Also
|
38
34
|
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lively
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain:
|
11
10
|
- |
|
@@ -37,7 +36,7 @@ cert_chain:
|
|
37
36
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
38
37
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
39
38
|
-----END CERTIFICATE-----
|
40
|
-
date:
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
41
40
|
dependencies:
|
42
41
|
- !ruby/object:Gem::Dependency
|
43
42
|
name: falcon
|
@@ -59,14 +58,14 @@ dependencies:
|
|
59
58
|
requirements:
|
60
59
|
- - "~>"
|
61
60
|
- !ruby/object:Gem::Version
|
62
|
-
version: '0.
|
61
|
+
version: '0.17'
|
63
62
|
type: :runtime
|
64
63
|
prerelease: false
|
65
64
|
version_requirements: !ruby/object:Gem::Requirement
|
66
65
|
requirements:
|
67
66
|
- - "~>"
|
68
67
|
- !ruby/object:Gem::Version
|
69
|
-
version: '0.
|
68
|
+
version: '0.17'
|
70
69
|
- !ruby/object:Gem::Dependency
|
71
70
|
name: xrb
|
72
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,14 +80,15 @@ dependencies:
|
|
81
80
|
- - ">="
|
82
81
|
- !ruby/object:Gem::Version
|
83
82
|
version: '0'
|
84
|
-
description:
|
85
|
-
email:
|
86
83
|
executables:
|
87
84
|
- lively
|
88
85
|
extensions: []
|
89
86
|
extra_rdoc_files: []
|
90
87
|
files:
|
91
88
|
- bin/lively
|
89
|
+
- context/getting-started.md
|
90
|
+
- context/index.yaml
|
91
|
+
- context/worms-tutorial.md
|
92
92
|
- lib/lively.rb
|
93
93
|
- lib/lively/application.rb
|
94
94
|
- lib/lively/assets.rb
|
@@ -118,7 +118,6 @@ licenses:
|
|
118
118
|
metadata:
|
119
119
|
documentation_uri: https://socketry.github.io/lively/
|
120
120
|
source_code_uri: https://github.com/socketry/lively.git
|
121
|
-
post_install_message:
|
122
121
|
rdoc_options: []
|
123
122
|
require_paths:
|
124
123
|
- lib
|
@@ -126,15 +125,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
125
|
requirements:
|
127
126
|
- - ">="
|
128
127
|
- !ruby/object:Gem::Version
|
129
|
-
version: '3.
|
128
|
+
version: '3.2'
|
130
129
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
130
|
requirements:
|
132
131
|
- - ">="
|
133
132
|
- !ruby/object:Gem::Version
|
134
133
|
version: '0'
|
135
134
|
requirements: []
|
136
|
-
rubygems_version: 3.
|
137
|
-
signing_key:
|
135
|
+
rubygems_version: 3.6.9
|
138
136
|
specification_version: 4
|
139
137
|
summary: A simple client-server SPA framework.
|
140
138
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|