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.
Files changed (46) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +5 -0
  3. data/README.md +118 -0
  4. data/Rakefile +1 -0
  5. data/alchemist-server.gemspec +29 -0
  6. data/bin/alchemist-repl +37 -0
  7. data/bin/alchemist-server +10 -0
  8. data/lib/alchemist-server.rb +151 -0
  9. data/lib/alchemist-server/avatar.rb +82 -0
  10. data/lib/alchemist-server/commands/appear.rb +13 -0
  11. data/lib/alchemist-server/commands/base.rb +31 -0
  12. data/lib/alchemist-server/commands/basics.rb +12 -0
  13. data/lib/alchemist-server/commands/compounds.rb +13 -0
  14. data/lib/alchemist-server/commands/create.rb +15 -0
  15. data/lib/alchemist-server/commands/describe.rb +18 -0
  16. data/lib/alchemist-server/commands/directions.rb +48 -0
  17. data/lib/alchemist-server/commands/element.rb +16 -0
  18. data/lib/alchemist-server/commands/forge.rb +18 -0
  19. data/lib/alchemist-server/commands/formulate.rb +20 -0
  20. data/lib/alchemist-server/commands/inventory.rb +12 -0
  21. data/lib/alchemist-server/commands/location.rb +13 -0
  22. data/lib/alchemist-server/commands/lock.rb +12 -0
  23. data/lib/alchemist-server/commands/look.rb +14 -0
  24. data/lib/alchemist-server/commands/message.rb +15 -0
  25. data/lib/alchemist-server/commands/put.rb +16 -0
  26. data/lib/alchemist-server/commands/read.rb +18 -0
  27. data/lib/alchemist-server/commands/take.rb +17 -0
  28. data/lib/alchemist-server/commands/who.rb +18 -0
  29. data/lib/alchemist-server/curses/geography_window.rb +63 -0
  30. data/lib/alchemist-server/curses/glyph_window.rb +89 -0
  31. data/lib/alchemist-server/curses/item_window.rb +59 -0
  32. data/lib/alchemist-server/curses/messages_window.rb +43 -0
  33. data/lib/alchemist-server/curses/prompt_window.rb +35 -0
  34. data/lib/alchemist-server/direction.rb +28 -0
  35. data/lib/alchemist-server/element.rb +14 -0
  36. data/lib/alchemist-server/event.rb +43 -0
  37. data/lib/alchemist-server/formula.rb +24 -0
  38. data/lib/alchemist-server/geography.rb +111 -0
  39. data/lib/alchemist-server/outcome.rb +12 -0
  40. data/lib/alchemist-server/record.rb +51 -0
  41. data/lib/alchemist-server/server_handler.rb +143 -0
  42. data/lib/alchemist-server/version.rb +8 -0
  43. data/lib/alchemist-server/world.rb +206 -0
  44. data/lib/alchemist-server/world_history.rb +45 -0
  45. data/script/alchemist-curses +307 -0
  46. 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,14 @@
1
+ module Alchemist
2
+ class Element
3
+ include Record
4
+ record_attr :symbol, :name, :basic
5
+
6
+ def basic?
7
+ basic
8
+ end
9
+
10
+ def hex_code
11
+ "0x#{symbol.ord.to_s(16)}"
12
+ end
13
+ end
14
+ end
@@ -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,12 @@
1
+ module Alchemist
2
+ class Outcome
3
+ include Record
4
+ include Enumerable
5
+
6
+ record_attr :response,
7
+ :new_world,
8
+ :new_history,
9
+ :nearby_avatar_command
10
+ end
11
+ end
12
+
@@ -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
+