demiurge 0.2.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 +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/.yardopts +5 -0
- data/AUTHORS.txt +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONCEPTS.md +271 -0
- data/Gemfile +4 -0
- data/HACKING.md +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/RELOADING.md +94 -0
- data/Rakefile +10 -0
- data/SECURITY.md +103 -0
- data/WORLD_FILES.md +134 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/demiurge.gemspec +31 -0
- data/exe/demirun +16 -0
- data/lib/demiurge/action_item.rb +643 -0
- data/lib/demiurge/agent.rb +338 -0
- data/lib/demiurge/container.rb +194 -0
- data/lib/demiurge/dsl.rb +583 -0
- data/lib/demiurge/exception.rb +170 -0
- data/lib/demiurge/inert_state_item.rb +21 -0
- data/lib/demiurge/intention.rb +164 -0
- data/lib/demiurge/location.rb +85 -0
- data/lib/demiurge/notification_names.rb +93 -0
- data/lib/demiurge/tmx.rb +439 -0
- data/lib/demiurge/util.rb +67 -0
- data/lib/demiurge/version.rb +4 -0
- data/lib/demiurge/zone.rb +108 -0
- data/lib/demiurge.rb +812 -0
- metadata +165 -0
data/RELOADING.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Reloading, Debugging and Demiurge Engines
|
2
|
+
|
3
|
+
Demiurge tries to make it easy to reload World Files in
|
4
|
+
development. What's so complicated about that?
|
5
|
+
|
6
|
+
Where possible, Demiurge tries to just keep the same zones, locations,
|
7
|
+
agents and so on with the same action names. If you change the code
|
8
|
+
for something that already exists, great! Demiurge can easily put that
|
9
|
+
together and give you the new code.
|
10
|
+
|
11
|
+
Fabulous. All done, right?
|
12
|
+
|
13
|
+
Nope. What if you rename a zone? You probably don't *want* Demiurge to
|
14
|
+
try to guess that based on the line in the file, or the new name. If
|
15
|
+
it gets it right 60% of the time (which is optimistic)... Well, what
|
16
|
+
about the other 40%? Also, you won't be in the habit of noticing and
|
17
|
+
fixing it, which will occasionally be *really* bad.
|
18
|
+
|
19
|
+
So: expect this to be okay for development. Do *not* expect it to work
|
20
|
+
perfectly, nor for you to be able to test a bunch of development
|
21
|
+
changes with incremental reloads and then have it work perfectly in
|
22
|
+
production later.
|
23
|
+
|
24
|
+
State is hard. Reloading is hard. Demiurge has some ways to help a
|
25
|
+
little. But this is all still hard.
|
26
|
+
|
27
|
+
## Resetting
|
28
|
+
|
29
|
+
## Simple Best Practices
|
30
|
+
|
31
|
+
When Demiurge loads your World Files, it has to execute them as Ruby
|
32
|
+
code. That means if you do something as a side effect (print to
|
33
|
+
console, write a file) it gets done every time. So try not to do that.
|
34
|
+
|
35
|
+
## What Makes This Easier?
|
36
|
+
|
37
|
+
Players love big persistent games like Minecraft where everything they
|
38
|
+
do persists forever and they can make huge changes to the game. And
|
39
|
+
yet there are very few games like that. Why?
|
40
|
+
|
41
|
+
Partly because it's really, really hard to *test* changes to a world
|
42
|
+
like that. Wouldn't it be cool if you could add new lava-or-water-type
|
43
|
+
blocks to Minecraft and change how they worked? Maybe you could make
|
44
|
+
some kind of slime-or-pudding block that poured and flowed like that?
|
45
|
+
And then make pudding sculptures, like those wonderful towers or
|
46
|
+
fortresses you make with water or lava pouring over them in Minecraft
|
47
|
+
Creative Mode? Now, think about how you'd *test* those changes in a
|
48
|
+
big persistent Minecraft-type world where everybody was building stuff
|
49
|
+
all the time.
|
50
|
+
|
51
|
+
(For the same reason, a Minecraft MMO would be a *gigantic* pain to
|
52
|
+
create. Just sayin'.)
|
53
|
+
|
54
|
+
If you were making changes to how the pudding blocks worked, you'd
|
55
|
+
have players *howling* every time the flow changed and their
|
56
|
+
fortresses looked different. You can't just put that out there and
|
57
|
+
then mess with it -- the "mess with it" part is really hard in a big
|
58
|
+
persistent world. Players get really attached to every small change.
|
59
|
+
|
60
|
+
So what do you do? Well, one thing is to wipe the slate clean,
|
61
|
+
regularly. Minecraft doesn't guarantee that you know exactly where
|
62
|
+
every skeleton or creeper is, and mostly you don't expect to. Most
|
63
|
+
games are even more like that -- World of Warcraft knows where
|
64
|
+
monsters *spawn* and it knows their *routes*, but you don't expect to
|
65
|
+
come back an hour later and see anything you did persist. Dropped
|
66
|
+
items disappear, killed monsters come back.
|
67
|
+
|
68
|
+
By having a lot of non-persisted state (information the game is
|
69
|
+
allowed to wipe clean), you leave room for making changes. If an admin
|
70
|
+
messes around with exactly how monsters spawn (how many? what timing?
|
71
|
+
do they roam around a little? where?) that doesn't feel like a big
|
72
|
+
deal -- you're used to that information getting reset constantly, and
|
73
|
+
little changes to it feel like window dressing. You forgot exactly
|
74
|
+
which monsters you killed thirty seconds after you killed them because
|
75
|
+
you don't *expect* that to be useful to remember. The spawn points
|
76
|
+
might be important - especially if you're used to sneaking past them
|
77
|
+
or quietly taking a route that keeps you out of the way of the big
|
78
|
+
nasties. But even changing those spawn points feels "fair" - it's
|
79
|
+
different from losing track of, say, how much gold your character
|
80
|
+
currently has, or exactly which blocks you put where in Minecraft to
|
81
|
+
make a shelter.
|
82
|
+
|
83
|
+
In general, you should make it clear when there's information you
|
84
|
+
won't be persisting. If you need to wipe out dropped weapons on the
|
85
|
+
floor (you probably do) then start that decay process *early* - you
|
86
|
+
want folks to know it's possible so that it's not a horrible surprise
|
87
|
+
when it happens.
|
88
|
+
|
89
|
+
And you want to persist as little information as you can get away
|
90
|
+
with. Any part of the game world that you (the admin) are allowed to
|
91
|
+
play with or reset is information you don't need to keep intact as
|
92
|
+
precious player data.
|
93
|
+
|
94
|
+
|
data/Rakefile
ADDED
data/SECURITY.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Security and User-Supplied Code
|
2
|
+
|
3
|
+
Demiurge is designed for an environment with very high-privilege code
|
4
|
+
(e.g. Demiurge itself, framework code, setup code) and somewhat
|
5
|
+
lower-privilege code (e.g. world files, certain player actions,
|
6
|
+
display code like particles.) It is *not* designed to let you just
|
7
|
+
"drop in" untrusted code and expect everything will be fine, basically
|
8
|
+
ever.
|
9
|
+
|
10
|
+
## Sandboxing
|
11
|
+
|
12
|
+
In general, "sandboxing" code to prevent it from doing things you
|
13
|
+
don't want is very hard. Ruby does nothing to make this easier - only
|
14
|
+
a few languages are easy to "sandbox", with JavaScript being by far
|
15
|
+
the most advanced in that specific respect -- it's designed to run in
|
16
|
+
the browser environment in a very specifically sandboxed way.
|
17
|
+
|
18
|
+
There are a few Ruby sandbox implementations (the current leader seems
|
19
|
+
to be [JRuby Sandbox]("https://github.com/jwo/jruby-sandbox")), but
|
20
|
+
in general they're hard to get right. Proving that a particular Ruby
|
21
|
+
snippet can't access a particular class (say ObjectSpace, which gives
|
22
|
+
full access to 100% of everything, or any flavor of "eval," or
|
23
|
+
"binding," or...) is very, very hard.
|
24
|
+
|
25
|
+
So: the current Demiurge codebase makes a much simpler assumption:
|
26
|
+
there are two overall flavors of code and neither is safe.
|
27
|
+
|
28
|
+
The first flavor is administrative code. Anything that runs with full
|
29
|
+
privilege (actions marked as full-privilege, anything in a framework
|
30
|
+
file or other non-World dot-rb file) must be examined very
|
31
|
+
carefully. These are full-danger code that can do absolutely anything,
|
32
|
+
and you must trust such code as much as the code you wrote for
|
33
|
+
yourself. They can do anything you can do, and running them "just for
|
34
|
+
this one person" without fully examining them is a terrible, terrible
|
35
|
+
idea.
|
36
|
+
|
37
|
+
## World Code
|
38
|
+
|
39
|
+
The second flavor is world code. Non-administrative zones, actions for
|
40
|
+
locations and agents and other similar code is world code. It runs
|
41
|
+
with more restrictions. It doesn't have methods to directly call most
|
42
|
+
dangerous operations like reloading your world from a state dump or
|
43
|
+
reloading a zone. You still have to examine it - a creative Ruby coder
|
44
|
+
can potentially get at just about anything they want to. But the basic
|
45
|
+
operations it provides are less dangerous, and a very simple chunk of
|
46
|
+
world code will normally not be able to do anything *too* awful.
|
47
|
+
|
48
|
+
However, the
|
49
|
+
[Halting Problem](https://en.wikipedia.org/wiki/Halting_problem)
|
50
|
+
guarantees that we can't make many guarantees about what code does,
|
51
|
+
and we certainly can't be sure it returns in a crisp, predictable
|
52
|
+
manner in general. So: assume that even world code, even very simple
|
53
|
+
world code, can cause you significant problems.
|
54
|
+
|
55
|
+
## Special Cases
|
56
|
+
|
57
|
+
There are very restricted cases where you can be sure that everything
|
58
|
+
is fine. As an intentionally-silly example, if you let the player
|
59
|
+
supply a six-digit hexadecimal color value and then apply it to be the
|
60
|
+
color of something using CSS... That's pretty darn securable. If you
|
61
|
+
want to make that safe, you totally can.
|
62
|
+
|
63
|
+
And as an intermediate-difficulty thing, it's not hard to come up with
|
64
|
+
a definition language for things like particle effects that only have
|
65
|
+
a few operations, limited conditionals and no loops, which are
|
66
|
+
essentially safe. At most a bad world-builder might be able to slow
|
67
|
+
down people's browsers, which isn't a huge deal.
|
68
|
+
|
69
|
+
But in general, if a language is [Turing Complete](https://en.wikipedia.org/wiki/Turing_completeness) then there's a way
|
70
|
+
to make it misbehave in a way you can't automatically detect. Which
|
71
|
+
means any language interesting enough to build a world in is also
|
72
|
+
interesting enough to cause you lots of problems with securing it
|
73
|
+
automatically.
|
74
|
+
|
75
|
+
It's still not a bad idea to build special cases. Those are where
|
76
|
+
you'll be able to let *players* supply content without carefully
|
77
|
+
vetting it. If you need world-builders to write zones in Ruby then you
|
78
|
+
need to have somebody review the code manually - there's not going to
|
79
|
+
be an enforcement system that's good enough to catch all the
|
80
|
+
problems. But if you want players to color their own gear or even make
|
81
|
+
up their own custom particle effects, that's probably not a problem.
|
82
|
+
|
83
|
+
It's surprising how much you can do with a special-purpose language or
|
84
|
+
format for just a few specific things. CSS in your browser is an
|
85
|
+
example of how far you can take this idea, though these days CSS is
|
86
|
+
Turing Complete too...
|
87
|
+
|
88
|
+
## How Do We Do It Right?
|
89
|
+
|
90
|
+
There is an eventual solution that could help this a fair bit, at a
|
91
|
+
cost of performance and complexity. By running a separate Ruby process
|
92
|
+
with limited privilege, it could become possible to halt a runaway
|
93
|
+
chunk of world code and watch its operations more carefully - it's
|
94
|
+
always easier to monitor a chunk of Ruby code from *outside* that Ruby
|
95
|
+
process.
|
96
|
+
|
97
|
+
That gets around problems with the various sandbox libraries -- how do
|
98
|
+
you avoid them using 100% CPU and never returning? How do you get
|
99
|
+
around them allocating unlimited memory? These aren't problems you can
|
100
|
+
get around with a Ruby-space sandbox library, not really. You need to
|
101
|
+
run the code evaluation in a separate process so that you can limit
|
102
|
+
those things without putting your limiting code in the same Ruby
|
103
|
+
interpreter, where "bad actor" code can get at it.
|
data/WORLD_FILES.md
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# Demiurge World Files
|
2
|
+
|
3
|
+
Demiurge includes a very convenient Ruby DSL (Domain-Specific
|
4
|
+
Language) for declaring your zones, locations, agents and so on. These
|
5
|
+
are referred to as World Files, and are by far the easiest and most
|
6
|
+
common way to create a simulation using Demiurge.
|
7
|
+
|
8
|
+
Unfortunately, the Builder classes in the API documentation aren't a
|
9
|
+
great way to document the World File syntax and how to use it. There
|
10
|
+
are some good examples, but many of them are either too complicated,
|
11
|
+
or intentionally weird. For instance, the test/ directory is full of
|
12
|
+
World File examples that are designed to test weird corner cases or
|
13
|
+
generate errors or both.
|
14
|
+
|
15
|
+
This is a quick guide to using World Files. It's not full and
|
16
|
+
comprehensive. But it may provide a kick-start, after which the
|
17
|
+
Builder classes won't seem quite so bizarre and inscrutable.
|
18
|
+
|
19
|
+
Normally World Files are stored in a directory in your game. I like
|
20
|
+
calling it "world". If you used multiple Demiurge engines for some
|
21
|
+
reason, you'd probably want to store their two sets of World Files in
|
22
|
+
two different directories. But for a single Engine, you'd normally
|
23
|
+
want a single directory of World Files.
|
24
|
+
|
25
|
+
## Getting Started
|
26
|
+
|
27
|
+
Your game will need to load its World Files somehow. Here's a good
|
28
|
+
simple example of how to do that:
|
29
|
+
|
30
|
+
Dir["**/world/extensions/**/*.rb"].each do |ruby_ext|
|
31
|
+
require_relative ruby_ext
|
32
|
+
end
|
33
|
+
@engine = Demiurge.engine_from_dsl_files *Dir["world/*.rb"]
|
34
|
+
|
35
|
+
This example doesn't try to do any kind of sorting to load the files
|
36
|
+
in a particular order. In other words, you'll need to do something
|
37
|
+
fancier if your game gets big.
|
38
|
+
|
39
|
+
It also assumes that all your World Files are in the top-level
|
40
|
+
directory, which won't stay true for long either. So: you can start
|
41
|
+
with this example, but you'll need to change it later.
|
42
|
+
|
43
|
+
## Your First Zone
|
44
|
+
|
45
|
+
Here's an example file containing a very simple zone for use with
|
46
|
+
Demiurge-CreateJS:
|
47
|
+
|
48
|
+
zone "trackless island", "type" => "TmxZone" do
|
49
|
+
tmx_location "start location" do
|
50
|
+
manasource_tile_layout "tmx/trackless_island_main.tmx"
|
51
|
+
description "A Mysterious Island"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
If you're using Demiurge-CreateJS (aka DCJS) then you'll probably want
|
56
|
+
to use 2D tile-based locations, also called TmxLocations. Those are
|
57
|
+
the ones you can generate using the Tiled map editor. See
|
58
|
+
Demiurge-CreateJS for more details.
|
59
|
+
|
60
|
+
If you're using Demiurge on your own projects without DCJS, you can do
|
61
|
+
that too. Here's an example administrative zone containing a player
|
62
|
+
definition:
|
63
|
+
|
64
|
+
# Player information can be stored here. In general, if you want
|
65
|
+
# information to stick around it needs a StateItem. An "inert"
|
66
|
+
# StateItem just means it doesn't change on its own or take any
|
67
|
+
# actions.
|
68
|
+
inert "players"
|
69
|
+
|
70
|
+
zone "adminzone" do
|
71
|
+
agent "player template" do
|
72
|
+
# No position or location, so it's instantiable.
|
73
|
+
|
74
|
+
# Special action that gets performed on character creation
|
75
|
+
define_action("create", "engine_code" => true) do
|
76
|
+
if ["bob", "angelbob", "noah"].include?(item.name)
|
77
|
+
item.state["admin"] = true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
define_action("statedump", "tags" => ["admin", "player_action"]) do
|
82
|
+
dump_state
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
You can create interesting object interactions with actions. You can
|
88
|
+
see a few actions above, but they're just the tip of the iceberg.
|
89
|
+
Here's a fun example where an agent can kick some stones down a cliff
|
90
|
+
and they'll start a mini-avalanche. The bigger the initial kick, the
|
91
|
+
bigger (by far) the total number of stones falling down:
|
92
|
+
|
93
|
+
zone "echoing cliffs" do
|
94
|
+
location "cliffside" do
|
95
|
+
|
96
|
+
agent "stone kicker" do
|
97
|
+
define_action "kick stones" do |how_many|
|
98
|
+
notification type: "echoing noise", "stone size": how_many, location: item.location_name
|
99
|
+
how_many -= 1
|
100
|
+
if how_many > 0
|
101
|
+
action "kick stones", how_many
|
102
|
+
action "kick stones", how_many
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
Each action block (the do/end part) is just Ruby code, so you can do
|
110
|
+
whatever Ruby can do there. The hard part is just figuring out what
|
111
|
+
information you can work with, and what you're allowed to do with
|
112
|
+
it. The methods above called "notification", "action" and "dump_state"
|
113
|
+
above are all standard and can be found in the BlockRunner classes -
|
114
|
+
that's AgentBlockRunner, ActionItemBlockRunner and EngineBlockRunner,
|
115
|
+
depending on how you run it. Agents get the AgentBlockRunner,
|
116
|
+
ActionItems get the ActionItemBlockRunner, and you only get the
|
117
|
+
EngineBlockRunner if you specifically pass the ":engine_code" option
|
118
|
+
to your action.
|
119
|
+
|
120
|
+
## Security
|
121
|
+
|
122
|
+
There's a SECURITY guide in its own file. But the short version is:
|
123
|
+
keep in mind that World Files are *not* secure. You should read
|
124
|
+
through any World File that somebody else gives you completely before
|
125
|
+
you add it to your game. A bad World File can steal your in-game
|
126
|
+
passwords and code. It can even attack other programs running on the
|
127
|
+
same server as your game (which might be your home computer, where you
|
128
|
+
do your banking.) It can send somebody else your data, and it can
|
129
|
+
download other code from the Internet if it's connected to the
|
130
|
+
Internet.
|
131
|
+
|
132
|
+
Do *not* accept unknown World Files from other people. It's like
|
133
|
+
running a downloaded .EXE on a Windows machine - it can do whatever
|
134
|
+
your computer can, and you really shouldn't let it.
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "demiurge"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/demiurge.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'demiurge/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "demiurge"
|
8
|
+
spec.version = Demiurge::VERSION
|
9
|
+
spec.authors = ["Noah Gibbs"]
|
10
|
+
spec.email = ["the.codefolio.guy@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A creator and manager for game rules and state.}
|
13
|
+
spec.description = %q{A creator and manager for game rules and state, separate from displaying and controlling the game. The idea is that a primarily-simulation game may be written using Ruby rules and a connection to the Demiurge process, while control and display are handled separately. This approach is primarily useful for 'simulation' games rather than fast-reflex 'twitch' games.}
|
14
|
+
spec.homepage = "https://github.com/noahgibbs/demiurge"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_runtime_dependency "multi_json", "~>1.12"
|
25
|
+
spec.add_runtime_dependency "tmx", "~>0.1.5"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
30
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
31
|
+
end
|
data/exe/demirun
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby -w -I./lib
|
2
|
+
|
3
|
+
require "demiurge"
|
4
|
+
require "demiurge/dsl"
|
5
|
+
require "demiurge/tmx"
|
6
|
+
|
7
|
+
require "multi_json"
|
8
|
+
|
9
|
+
if ARGV.size < 1
|
10
|
+
raise "Please give at least one Demiurge DSL file to parse!"
|
11
|
+
end
|
12
|
+
|
13
|
+
engine = Demiurge.engine_from_dsl_files(*ARGV)
|
14
|
+
engine.finished_init
|
15
|
+
|
16
|
+
loop { engine.advance_one_tick }
|