alchemist-server 0.0.1
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.
- data/.gitignore +5 -0
- data/Gemfile +5 -0
- data/README.md +118 -0
- data/Rakefile +1 -0
- data/alchemist-server.gemspec +29 -0
- data/bin/alchemist-repl +37 -0
- data/bin/alchemist-server +10 -0
- data/lib/alchemist-server.rb +151 -0
- data/lib/alchemist-server/avatar.rb +82 -0
- data/lib/alchemist-server/commands/appear.rb +13 -0
- data/lib/alchemist-server/commands/base.rb +31 -0
- data/lib/alchemist-server/commands/basics.rb +12 -0
- data/lib/alchemist-server/commands/compounds.rb +13 -0
- data/lib/alchemist-server/commands/create.rb +15 -0
- data/lib/alchemist-server/commands/describe.rb +18 -0
- data/lib/alchemist-server/commands/directions.rb +48 -0
- data/lib/alchemist-server/commands/element.rb +16 -0
- data/lib/alchemist-server/commands/forge.rb +18 -0
- data/lib/alchemist-server/commands/formulate.rb +20 -0
- data/lib/alchemist-server/commands/inventory.rb +12 -0
- data/lib/alchemist-server/commands/location.rb +13 -0
- data/lib/alchemist-server/commands/lock.rb +12 -0
- data/lib/alchemist-server/commands/look.rb +14 -0
- data/lib/alchemist-server/commands/message.rb +15 -0
- data/lib/alchemist-server/commands/put.rb +16 -0
- data/lib/alchemist-server/commands/read.rb +18 -0
- data/lib/alchemist-server/commands/take.rb +17 -0
- data/lib/alchemist-server/commands/who.rb +18 -0
- data/lib/alchemist-server/curses/geography_window.rb +63 -0
- data/lib/alchemist-server/curses/glyph_window.rb +89 -0
- data/lib/alchemist-server/curses/item_window.rb +59 -0
- data/lib/alchemist-server/curses/messages_window.rb +43 -0
- data/lib/alchemist-server/curses/prompt_window.rb +35 -0
- data/lib/alchemist-server/direction.rb +28 -0
- data/lib/alchemist-server/element.rb +14 -0
- data/lib/alchemist-server/event.rb +43 -0
- data/lib/alchemist-server/formula.rb +24 -0
- data/lib/alchemist-server/geography.rb +111 -0
- data/lib/alchemist-server/outcome.rb +12 -0
- data/lib/alchemist-server/record.rb +51 -0
- data/lib/alchemist-server/server_handler.rb +143 -0
- data/lib/alchemist-server/version.rb +8 -0
- data/lib/alchemist-server/world.rb +206 -0
- data/lib/alchemist-server/world_history.rb +45 -0
- data/script/alchemist-curses +307 -0
- metadata +148 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
module Alchemist
|
2
|
+
module Curses
|
3
|
+
class ItemWindow
|
4
|
+
include FFI::NCurses
|
5
|
+
|
6
|
+
def initialize(label, size, line, column)
|
7
|
+
@label = label
|
8
|
+
@width = label.length + size * 2
|
9
|
+
@win = newwin 1, @width, line, column
|
10
|
+
@items = ""
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(items)
|
14
|
+
@items = items.force_encoding Encoding::UTF_8
|
15
|
+
draw
|
16
|
+
end
|
17
|
+
|
18
|
+
def draw
|
19
|
+
wclear @win
|
20
|
+
wprintw @win, "#{@label}#{@items.pad_to_unicode_monospace}"
|
21
|
+
wrefresh @win
|
22
|
+
end
|
23
|
+
|
24
|
+
def have_user_select
|
25
|
+
wmove @win, 0, @label.length
|
26
|
+
wrefresh @win
|
27
|
+
|
28
|
+
while (c = getch) != KEY_RETURN
|
29
|
+
case c
|
30
|
+
when KEY_LEFT
|
31
|
+
y,x = getyx @win
|
32
|
+
new_x = [x - 2, @label.length].max
|
33
|
+
wmove @win, 0, new_x
|
34
|
+
wrefresh @win
|
35
|
+
|
36
|
+
when KEY_RIGHT
|
37
|
+
y,x = getyx @win
|
38
|
+
new_x = [x + 2, @width - 1].min
|
39
|
+
wmove @win, 0, new_x
|
40
|
+
wrefresh @win
|
41
|
+
|
42
|
+
when KEY_ESCAPE
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
y,x = getyx @win
|
48
|
+
|
49
|
+
item_at x
|
50
|
+
end
|
51
|
+
|
52
|
+
def item_at(x)
|
53
|
+
inv_index = (x - @label.length) / 2
|
54
|
+
@items[inv_index]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Alchemist
|
2
|
+
module Curses
|
3
|
+
class MessagesWindow
|
4
|
+
include FFI::NCurses
|
5
|
+
|
6
|
+
def initialize(height, width, line, col)
|
7
|
+
@height = height
|
8
|
+
@width = width
|
9
|
+
|
10
|
+
@offset = 0
|
11
|
+
@messages = []
|
12
|
+
|
13
|
+
@win = newwin height, width, line, col
|
14
|
+
end
|
15
|
+
|
16
|
+
def draw
|
17
|
+
wmove @win, 0, 0
|
18
|
+
wclear @win
|
19
|
+
wprintw @win, messages_to_show.join("\n")
|
20
|
+
wrefresh @win
|
21
|
+
end
|
22
|
+
|
23
|
+
def messages_to_show
|
24
|
+
@messages[@offset,@height] || []
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(messages)
|
28
|
+
@messages = messages
|
29
|
+
draw
|
30
|
+
end
|
31
|
+
|
32
|
+
def scroll_up
|
33
|
+
@offset = [@offset-1, 0].max
|
34
|
+
draw
|
35
|
+
end
|
36
|
+
|
37
|
+
def scroll_down
|
38
|
+
@offset = [@offset+1, @messages.length - height].min
|
39
|
+
draw
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Alchemist
|
2
|
+
module Curses
|
3
|
+
class PromptWindow
|
4
|
+
include FFI::NCurses
|
5
|
+
|
6
|
+
def initialize(line, col, width)
|
7
|
+
@win = newwin 1, width, line, col
|
8
|
+
end
|
9
|
+
|
10
|
+
def ask(label)
|
11
|
+
wclear @win
|
12
|
+
wmove @win, 0, 0
|
13
|
+
wprintw @win, label
|
14
|
+
wprintw @win, ': '
|
15
|
+
wrefresh @win
|
16
|
+
|
17
|
+
answer = ''
|
18
|
+
|
19
|
+
while (c = wgetch @win) != "\n".ord
|
20
|
+
if c > 0
|
21
|
+
answer << c
|
22
|
+
wprintw @win, (''<<c)
|
23
|
+
end
|
24
|
+
|
25
|
+
wrefresh @win
|
26
|
+
end
|
27
|
+
|
28
|
+
wclear @win
|
29
|
+
|
30
|
+
answer.strip
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Alchemist
|
2
|
+
module Direction
|
3
|
+
module North
|
4
|
+
def self.move_from(x, y)
|
5
|
+
[x, y - 1]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module South
|
10
|
+
def self.move_from(x, y)
|
11
|
+
[x, y + 1]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module East
|
16
|
+
def self.move_from(x, y)
|
17
|
+
[x + 1, y]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module West
|
22
|
+
def self.move_from(x, y)
|
23
|
+
[x - 1, y]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Alchemist
|
2
|
+
class Event
|
3
|
+
attr_reader :command_string
|
4
|
+
|
5
|
+
def initialize(command_string, time)
|
6
|
+
@command_string = command_string
|
7
|
+
@time = time
|
8
|
+
end
|
9
|
+
|
10
|
+
def happen(history)
|
11
|
+
avatar_name, command, *args = @command_string.split /\s+/
|
12
|
+
|
13
|
+
if command_mod = COMMANDS.detect { |c| match_command? command, c }
|
14
|
+
command_mod.run avatar_name, history, *args
|
15
|
+
else
|
16
|
+
Outcome.new response: "error Unknown Command: #{command}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
<<-str
|
22
|
+
#{@command_string}
|
23
|
+
#{@time}
|
24
|
+
str
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def match_command?(command_name, command)
|
30
|
+
command_name =~ /^#{command.pattern}/
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.parse(string)
|
34
|
+
command_string, time, world_string = string.split("\n",3)
|
35
|
+
new command_string, Time.parse(time)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.genesis
|
39
|
+
new 'genesis', Time.now
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Alchemist
|
3
|
+
class Formula
|
4
|
+
attr_reader :elem_1, :elem_2, :result
|
5
|
+
|
6
|
+
def initialize(elem_1, elem_2, result)
|
7
|
+
@elem_1 = elem_1
|
8
|
+
@elem_2 = elem_2
|
9
|
+
@result = result
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"⨍ #{@elem_1}#{@elem_2}#{@result}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
other.class == self.class &&
|
18
|
+
other.elem_1 == elem_1 &&
|
19
|
+
other.elem_2 == elem_2 &&
|
20
|
+
other.result == result
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Alchemist
|
2
|
+
class Geography
|
3
|
+
def initialize(locations = {})
|
4
|
+
@locations = case locations
|
5
|
+
when Hamster::Hash then locations
|
6
|
+
else Hamster.hash locations
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def at(x,y)
|
11
|
+
locations[Loc[x,y]]
|
12
|
+
end
|
13
|
+
|
14
|
+
def take(x,y)
|
15
|
+
Geography.new locations.delete(Loc[x,y])
|
16
|
+
end
|
17
|
+
|
18
|
+
def put(x,y,c)
|
19
|
+
loc = Loc[x,y]
|
20
|
+
|
21
|
+
if locations.key? loc
|
22
|
+
raise "#{locations[loc]} is already at #{x},#{y}"
|
23
|
+
end
|
24
|
+
|
25
|
+
Geography.new locations.put(loc, c[0..1])
|
26
|
+
end
|
27
|
+
|
28
|
+
def string_around(x,y,range)
|
29
|
+
string_within x - range,
|
30
|
+
y - range,
|
31
|
+
x + range,
|
32
|
+
y + range
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
(min_x, min_y), (max_x, max_y) = dimensions
|
37
|
+
string_within min_x, min_y, max_x, max_y
|
38
|
+
end
|
39
|
+
|
40
|
+
def string_within(min_x, min_y, max_x, max_y)
|
41
|
+
if min_x && min_y && max_x && max_y
|
42
|
+
(min_y..max_y).map do |y|
|
43
|
+
(min_x..max_x).map do |x|
|
44
|
+
at(x, y) || ' '
|
45
|
+
end.join('')
|
46
|
+
end.join("\n")
|
47
|
+
else
|
48
|
+
''
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def dimensions
|
54
|
+
points = locations.keys
|
55
|
+
xs,ys = points.map(&:x), points.map(&:y)
|
56
|
+
|
57
|
+
[[xs.min, ys.min],[xs.max,ys.max]]
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def locations
|
63
|
+
@locations
|
64
|
+
end
|
65
|
+
|
66
|
+
def offset(x,y)
|
67
|
+
w,h = dimensions
|
68
|
+
y*(w + 1) + x
|
69
|
+
end
|
70
|
+
|
71
|
+
class Loc
|
72
|
+
attr_reader :x, :y
|
73
|
+
|
74
|
+
def self.[](*args)
|
75
|
+
new *args
|
76
|
+
end
|
77
|
+
|
78
|
+
def initialize(x,y)
|
79
|
+
@x = x
|
80
|
+
@y = y
|
81
|
+
end
|
82
|
+
|
83
|
+
def hash
|
84
|
+
(@x.hash * 31) + @y.hash
|
85
|
+
end
|
86
|
+
|
87
|
+
def within?(min, max)
|
88
|
+
min.x <= x && x <= max.x &&
|
89
|
+
min.y <= y && y <= max.y
|
90
|
+
end
|
91
|
+
|
92
|
+
def distance(loc)
|
93
|
+
[
|
94
|
+
(loc.x - x).abs,
|
95
|
+
(loc.y - y).abs
|
96
|
+
].max
|
97
|
+
end
|
98
|
+
|
99
|
+
def ==(other)
|
100
|
+
other.class == self.class &&
|
101
|
+
other.x == x &&
|
102
|
+
other.y == y
|
103
|
+
end
|
104
|
+
|
105
|
+
def eql?(other)
|
106
|
+
self == other
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Alchemist
|
2
|
+
module Record
|
3
|
+
def self.included(mod)
|
4
|
+
mod.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(attrs = {})
|
8
|
+
@attrs = Record.hamsterize(attrs).slice(*attr_names)
|
9
|
+
end
|
10
|
+
|
11
|
+
def update(attr_changes)
|
12
|
+
self.class.new @attrs.merge(Record.hamsterize(attr_changes))
|
13
|
+
end
|
14
|
+
|
15
|
+
def read(attr)
|
16
|
+
@attrs[attr]
|
17
|
+
end
|
18
|
+
|
19
|
+
def attr_names
|
20
|
+
self.class.attr_names
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.hamsterize(hash)
|
24
|
+
case hash
|
25
|
+
when Hamster::Hash then hash
|
26
|
+
else Hamster.hash hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def attr_names
|
32
|
+
@attr_names ||= []
|
33
|
+
end
|
34
|
+
|
35
|
+
def attr_names=(attrs)
|
36
|
+
@attr_names = attrs
|
37
|
+
end
|
38
|
+
|
39
|
+
def record_attr(*attrs)
|
40
|
+
attrs.each do |attr|
|
41
|
+
class_eval %{
|
42
|
+
def #{attr}
|
43
|
+
read :#{attr}
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end
|
47
|
+
self.attr_names += attrs
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Alchemist
|
2
|
+
module ServerHandler
|
3
|
+
def self.new(world_file)
|
4
|
+
puts "Loading history...."
|
5
|
+
history = Alchemist::Server.load_history world_file
|
6
|
+
|
7
|
+
Module.new do
|
8
|
+
include Methods
|
9
|
+
|
10
|
+
@world_file = world_file
|
11
|
+
@history = history
|
12
|
+
@connections = []
|
13
|
+
|
14
|
+
class <<self
|
15
|
+
attr_reader :world_file
|
16
|
+
attr_accessor :history
|
17
|
+
attr_accessor :connections
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(mod)
|
21
|
+
mod.instance_variable_set :@state, self
|
22
|
+
def mod.state; @state; end
|
23
|
+
end
|
24
|
+
|
25
|
+
def world_file
|
26
|
+
self.class.state.world_file
|
27
|
+
end
|
28
|
+
|
29
|
+
def history
|
30
|
+
self.class.state.history
|
31
|
+
end
|
32
|
+
|
33
|
+
def history=(h)
|
34
|
+
self.class.state.history = h
|
35
|
+
end
|
36
|
+
|
37
|
+
def connections
|
38
|
+
self.class.state.connections
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Methods
|
44
|
+
include ::EventMachine::Protocols::LineProtocol
|
45
|
+
|
46
|
+
attr_reader :name
|
47
|
+
|
48
|
+
def post_init
|
49
|
+
connections << self
|
50
|
+
send_line "Welcome alchemical friend. What is your name?"
|
51
|
+
end
|
52
|
+
|
53
|
+
def unbind
|
54
|
+
connections.delete self
|
55
|
+
end
|
56
|
+
|
57
|
+
def receive_line(line)
|
58
|
+
t = Benchmark.realtime do
|
59
|
+
response = process_line line
|
60
|
+
|
61
|
+
if response
|
62
|
+
send_line response
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "#{Time.now} #{line.split(' ').first} #{(t*1000).round}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_line(line)
|
70
|
+
if name
|
71
|
+
command = "#{name} #{line.chomp}"
|
72
|
+
outcome = Alchemist::Server.run_append command,
|
73
|
+
world_file,
|
74
|
+
history
|
75
|
+
|
76
|
+
self.history = outcome.new_history if outcome.new_history
|
77
|
+
|
78
|
+
if command = outcome.nearby_avatar_command
|
79
|
+
run_command_nearby command
|
80
|
+
end
|
81
|
+
|
82
|
+
outcome.response
|
83
|
+
else
|
84
|
+
possible_name = line.strip.split(' ').first
|
85
|
+
|
86
|
+
if possible_name && possible_name.length > 0
|
87
|
+
@name = possible_name
|
88
|
+
"hello #{name}"
|
89
|
+
else
|
90
|
+
"Please tell me your name."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
rescue => e
|
94
|
+
show_error e
|
95
|
+
"error #{e}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def run_command_nearby(command)
|
99
|
+
new_nearby = history.world.nearby_avatar_names name
|
100
|
+
old_nearby = begin
|
101
|
+
history.prior_world.try :nearby_avatar_names, name
|
102
|
+
rescue
|
103
|
+
# bail out if user wasn't in prior world
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
|
107
|
+
nearby = new_nearby | (old_nearby || [])
|
108
|
+
|
109
|
+
connections.select do |c|
|
110
|
+
begin
|
111
|
+
if nearby.include? c.name
|
112
|
+
c.run_unrecorded_command command
|
113
|
+
end
|
114
|
+
rescue => e
|
115
|
+
show_error e
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def show_error(e)
|
121
|
+
$stderr.puts "#{e.class}: #{e.message}"
|
122
|
+
$stderr.puts e.backtrace
|
123
|
+
end
|
124
|
+
|
125
|
+
def run_unrecorded_command(command)
|
126
|
+
outcome = command.run name, history
|
127
|
+
|
128
|
+
if r = outcome.response
|
129
|
+
send_line r
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def send_line(data)
|
134
|
+
send_data data
|
135
|
+
|
136
|
+
if data[-1] != "\n"
|
137
|
+
send_data "\n"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|