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,8 @@
1
+ module Alchemist
2
+ module Server
3
+ if defined? VERSION
4
+ raise "OMG"
5
+ end
6
+ VERSION = "0.0.1"
7
+ end
8
+ end
@@ -0,0 +1,206 @@
1
+ module Alchemist
2
+ class World
3
+ include Record
4
+ LOOK_RANGE = 10
5
+ record_attr :avatars,
6
+ :formulas,
7
+ :geography,
8
+ :elements,
9
+ :locked
10
+
11
+ def to_s
12
+ (avatars.to_a.map(&:to_s) + [geography]).join("\n")
13
+ end
14
+
15
+ def new_avatar(name)
16
+ a = Avatar.new name: name,
17
+ x: 0,
18
+ y: 0,
19
+ inventory: "",
20
+ messages: Hamster.hash
21
+
22
+ if !avatars.include? a
23
+ update avatars: avatars | [a]
24
+ end
25
+ end
26
+
27
+ def nearby_avatar_names(name)
28
+ nearby_avatars(avatar(name)).map &:name
29
+ end
30
+
31
+ def nearby_avatars(a)
32
+ avatars.select do |avatar|
33
+ avatar.near? a.location, LOOK_RANGE
34
+ end
35
+ end
36
+
37
+ def lock
38
+ raise "The world is already locked" if locked
39
+ update locked: true
40
+ end
41
+
42
+ def validate_element_symbol!(symbol)
43
+ if !Glyphs.strings.include? symbol
44
+ raise "#{symbol} is not a valid element symbol"
45
+ end
46
+ end
47
+
48
+ LOCKED_MESSAGE = "New basic elements can no longer be created. Try creating a compound instead"
49
+ def new_element(char, name)
50
+ raise LOCKED_MESSAGE if locked
51
+ validate_element_symbol! char
52
+
53
+ e = Element.new symbol: char, name: name, basic: true
54
+
55
+ update elements: elements.put(char, e)
56
+ end
57
+
58
+ def formulate(avatar_name, elem_1, elem_2, novel_elem, name)
59
+ validate_element_symbol! novel_elem
60
+
61
+ if formulas[novel_elem]
62
+ raise "There is already a formula for #{novel_elem}"
63
+
64
+ elsif elements[novel_elem]
65
+ raise "#{novel_elem} is a basic element!"
66
+
67
+ else
68
+ f = Formula.new elem_1, elem_2, novel_elem
69
+ e = Element.new symbol: novel_elem,
70
+ name: name,
71
+ basic: false
72
+
73
+ w = update formulas: formulas.put(novel_elem, f),
74
+ elements: elements.put(novel_elem, e)
75
+
76
+ w.forge(avatar_name, elem_1, elem_2, novel_elem)
77
+ end
78
+ end
79
+
80
+ def forge(avatar_name, elem_1, elem_2, result)
81
+ a = avatar avatar_name
82
+
83
+ if !a.has?(elem_1, elem_2)
84
+ raise "#{avatar_name} doesn't have the required elements"
85
+ end
86
+
87
+ f = Formula.new elem_1, elem_2, result
88
+
89
+ if formulas[result] == f
90
+ a_temp = a.remove_from_inventory elem_1, elem_2
91
+ a_prime = a_temp.add_to_inventory result
92
+
93
+ update avatars: avatars - [a] + [a_prime]
94
+ else
95
+ raise "Incorrect formula for #{result}"
96
+ end
97
+ end
98
+
99
+ def avatar(name)
100
+ avatars.detect { |a| a.name == name } ||
101
+ raise("#{name} isn't in the world")
102
+ end
103
+
104
+ def at(avatar_name)
105
+ a = avatar(avatar_name)
106
+ geography.at a.x, a.y
107
+ end
108
+
109
+ def look(avatar_name)
110
+ a = avatar(avatar_name)
111
+ geography.string_around a.x, a.y, LOOK_RANGE
112
+ end
113
+
114
+ def take(avatar_name)
115
+ a = avatar(avatar_name)
116
+
117
+ resource = geography.at a.x, a.y
118
+ new_a = a.add_to_inventory resource
119
+ new_g = geography.take a.x, a.y
120
+
121
+ update avatars: avatars - [a] + [new_a],
122
+ geography: new_g
123
+ end
124
+
125
+ def put(avatar_name, c)
126
+ a = avatar(avatar_name)
127
+
128
+ if a.has? c
129
+ new_g = geography.put a.x, a.y, c
130
+ new_a = a.remove_from_inventory c
131
+
132
+ update avatars: avatars - [a] + [new_a],
133
+ geography: new_g
134
+ else
135
+ raise "#{avatar_name} doesn't have #{c}"
136
+ end
137
+ end
138
+
139
+ def create(avatar_name, c)
140
+ a = avatar(avatar_name)
141
+ new_a = a.add_to_inventory c
142
+
143
+ element = elements[c]
144
+
145
+ if element.nil?
146
+ raise "Unknown element: #{c}."
147
+ elsif !element.basic
148
+ raise "#{c} is not a basic element."
149
+ end
150
+
151
+ update avatars: avatars - [a] + [new_a]
152
+ end
153
+
154
+ def move(avatar_name, direction)
155
+ a = avatar(avatar_name)
156
+ a_prime = a.move direction
157
+
158
+ update avatars: avatars - [a] + [a_prime]
159
+ end
160
+
161
+ def put_message(avatar_name, key, message)
162
+ a = avatar(avatar_name)
163
+ a_prime = a.put_message key, message
164
+
165
+ update avatars: avatars - [a] + [a_prime]
166
+ end
167
+
168
+ def messages_for(avatar_name)
169
+ a = avatar(avatar_name)
170
+ messengers = nearby_avatars a
171
+
172
+ messengers.sort_by(&:name).map do |m|
173
+ { m.name => m.message_list }
174
+ end.reduce({}, :merge)
175
+ end
176
+
177
+ def location(avatar_name)
178
+ a = avatar avatar_name
179
+ [a.x, a.y]
180
+ end
181
+
182
+ def dimensions
183
+ geography.dimensions
184
+ end
185
+
186
+ def basic_elements
187
+ elements.values.select(&:basic?)
188
+ end
189
+
190
+ def compound_elements
191
+ elements.values.reject(&:basic?)
192
+ end
193
+
194
+ def element(symbol)
195
+ elements[symbol]
196
+ end
197
+
198
+ def self.genesis
199
+ World.new avatars: [],
200
+ formulas: Hamster.hash,
201
+ geography: Geography.new,
202
+ elements: Hamster.hash
203
+ end
204
+ end
205
+ end
206
+
@@ -0,0 +1,45 @@
1
+ module Alchemist
2
+ class WorldHistory
3
+ attr_reader :world
4
+
5
+ def initialize(event, prior_history)
6
+ @event = event
7
+ @prior_history = prior_history
8
+
9
+ @world =
10
+ if @prior_history.nil?
11
+ World.genesis
12
+ else
13
+ outcome = @event.happen @prior_history
14
+ outcome.try(:new_world) || prior_world
15
+ end
16
+ end
17
+
18
+ def prior_world
19
+ @prior_history.try :world
20
+ end
21
+
22
+ def to_s
23
+ <<-str.strip
24
+ #{@prior_history}
25
+
26
+ #{@event}
27
+ str
28
+ end
29
+
30
+ def self.parse(string)
31
+ split_into_chronological_events(string).reduce(nil) do |history, event_string|
32
+ new Event.parse(event_string), history
33
+ end
34
+ end
35
+
36
+ def self.split_into_chronological_events(string)
37
+ string.split("\n\n").reject { |l| l =~ /^\s*$/ }
38
+ end
39
+
40
+ def self.genesis
41
+ new Event.genesis, nil
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $LOAD_PATH << "../lib"
5
+
6
+ require 'bundler'
7
+ Bundler.setup
8
+
9
+ require 'alchemist-server'
10
+ require 'ffi-ncurses'
11
+ require 'alchemist-server/curses/item_window'
12
+ require 'alchemist-server/curses/prompt_window'
13
+ require 'alchemist-server/curses/glyph_window'
14
+ require 'alchemist-server/curses/messages_window'
15
+ require 'alchemist-server/curses/geography_window'
16
+
17
+ host = ARGV[0] || 'localhost'
18
+ port = (ARGV[1] || 7900).to_i
19
+
20
+ def start(host, port)
21
+ EventMachine.run do
22
+ EventMachine.connect host, port, Handler
23
+ end
24
+ end
25
+
26
+ module Handler
27
+ include FFI::NCurses
28
+ include Alchemist::EventMachine::ClientProtocol
29
+
30
+ INVENTORY_LABEL = 'Inventory: '
31
+ INVENTORY_SIZE = 64
32
+
33
+ BASICS_LABEL = 'Basics: '
34
+ BASICS_SIZE = INVENTORY_SIZE
35
+
36
+ COMPOUNDS_LABEL = 'Compounds: '
37
+ COMPOUNDS_SIZE = INVENTORY_SIZE
38
+
39
+ AVATAR_COLOR_NUM = 1
40
+
41
+ def post_init
42
+ start_color
43
+ init_pair AVATAR_COLOR_NUM, Color::BLACK, Color::GREEN
44
+
45
+ @border_win = newwin 22, 45, 0, 0
46
+ @geo_win = Alchemist::Curses::GeographyWindow.new(
47
+ 1, 1, AVATAR_COLOR_NUM)
48
+
49
+ @location_win = newwin 1, 45, 23, 0
50
+
51
+ @prompt_win = Alchemist::Curses::PromptWindow.new(
52
+ 24, 0, 80)
53
+
54
+ @inventory_win = Alchemist::Curses::ItemWindow.new(
55
+ INVENTORY_LABEL,
56
+ INVENTORY_SIZE,
57
+ 25,
58
+ 0
59
+ )
60
+
61
+ @basics_win = Alchemist::Curses::ItemWindow.new(
62
+ BASICS_LABEL,
63
+ BASICS_SIZE,
64
+ 26,
65
+ 0
66
+ )
67
+
68
+ @compounds_win = Alchemist::Curses::ItemWindow.new(
69
+ COMPOUNDS_LABEL,
70
+ COMPOUNDS_SIZE,
71
+ 27,
72
+ 0
73
+ )
74
+
75
+ @error_win = newwin 1, 80, 28, 0
76
+ @glyph_win = Alchemist::Curses::GlyphWindow.new 0, 46
77
+
78
+ @messages_win = Alchemist::Curses::MessagesWindow.new(
79
+ 10, 80, 29, 0
80
+ )
81
+ end
82
+
83
+ def item_hover(item)
84
+ describe item
85
+ end
86
+
87
+ def draw_geo_border
88
+ lr = '|'.ord
89
+ tb = '-'.ord
90
+ c = '+'.ord
91
+
92
+ wborder @border_win,lr,lr,tb,tb,c,c,c,c
93
+ wrefresh @border_win
94
+ end
95
+
96
+ def handle_see(data)
97
+ if @location
98
+ @geo_win.update @location, data
99
+ end
100
+ end
101
+
102
+ def handle_location(data)
103
+ x, y = data.split(/\s+/).map &:to_i
104
+ @location = [x, y]
105
+ draw_location *@location
106
+ @geo_win.update_avatar @name, *@location
107
+ end
108
+
109
+ def draw_location(x, y)
110
+ wclear @location_win
111
+ wmove @location_win, 0, 0
112
+ wprintw @location_win, "Location: #{x},#{y}"
113
+ wrefresh @location_win
114
+ end
115
+
116
+ def handle_inventory(data)
117
+ @inventory_win.update data
118
+ move_to_avatar_position
119
+ end
120
+
121
+ def handle_basics(data)
122
+ @basics_win.update data
123
+ move_to_avatar_position
124
+ end
125
+
126
+ def handle_compounds(data)
127
+ @compounds_win.update data
128
+ move_to_avatar_position
129
+ end
130
+
131
+ def handle_messages(data)
132
+ @messages_win.update data.split("\n")
133
+ move_to_avatar_position
134
+ end
135
+
136
+ def move_to_avatar_position
137
+ @geo_win.move_to_avatar_position
138
+ end
139
+
140
+ def handle_element(message)
141
+ draw_error message.pad_to_unicode_monospace
142
+ end
143
+
144
+ def element(message)
145
+ draw_error message.pad_to_unicode_monospace
146
+ end
147
+
148
+ def handle_error(error)
149
+ draw_error error
150
+ end
151
+
152
+ def draw_error(error)
153
+ wmove @error_win, 0, 0
154
+ wprintw @error_win, "Last error: "
155
+ wprintw @error_win, error
156
+ wrefresh @error_win
157
+ move_to_avatar_position
158
+ end
159
+
160
+ def handle_avatars(data)
161
+ locations = data.each_line.map do |line|
162
+ name, x, y = line.split /\s+/
163
+ { name => [x.to_i,y.to_i] }
164
+ end
165
+
166
+ @geo_win.update_avatars locations.reduce :merge
167
+ end
168
+
169
+ def handle_welcome
170
+ EM.defer(-> { @prompt_win.ask "Enter your name" },
171
+ -> name do
172
+ @name = name
173
+ login name
174
+ end)
175
+ end
176
+
177
+ def handle_hello
178
+ printw "Appearing...\n"
179
+ refresh
180
+ appear
181
+ end
182
+
183
+ def handle_appeared
184
+ draw_geo_border
185
+ who
186
+ request_location
187
+ look
188
+ request_inventory
189
+ request_basics
190
+ request_compounds
191
+ read
192
+ action_loop
193
+ end
194
+
195
+ def act(char)
196
+ case char
197
+ when KEY_LEFT then move_action "west"
198
+ when KEY_RIGHT then move_action "east"
199
+ when KEY_UP then move_action "north"
200
+ when KEY_DOWN then move_action "south"
201
+ when 't'.ord then take_action
202
+ when 'p'.ord then put_action
203
+ when 'c'.ord then create_action
204
+ when 'e'.ord then element_action
205
+ when 'f'.ord then forge_action
206
+ when 'F'.ord then formulate_action
207
+ when 'm'.ord then post_message_action
208
+ when 'r'.ord then read
209
+ when 'd'.ord then describe_action
210
+ end
211
+ end
212
+
213
+ def move_action(direction)
214
+ move direction
215
+ look
216
+ end
217
+
218
+ def put_action
219
+ if element = @inventory_win.have_user_select
220
+ put element
221
+ end
222
+ end
223
+
224
+ def describe_action
225
+ if element = @inventory_win.have_user_select
226
+ describe element
227
+ end
228
+ end
229
+
230
+ def create_action
231
+ if element = @basics_win.have_user_select
232
+ create element
233
+ end
234
+ end
235
+
236
+ def element_action
237
+ if symbol = @glyph_win.have_user_select
238
+ name = @prompt_win.ask "New Element Name"
239
+ element symbol, name
240
+ request_basics
241
+ end
242
+ end
243
+
244
+ def forge_action
245
+ if (ingred_1 = @inventory_win.have_user_select) &&
246
+ (ingred_2 = @inventory_win.have_user_select) &&
247
+ (result = @compounds_win.have_user_select)
248
+ forge ingred_1, ingred_2, result
249
+ end
250
+ end
251
+
252
+ def formulate_action
253
+ if (ingred_1 = @inventory_win.have_user_select) &&
254
+ (ingred_2 = @inventory_win.have_user_select) &&
255
+ (result = @glyph_win.have_user_select)
256
+ name = @prompt_win.ask "Compound Name"
257
+ formulate ingred_1, ingred_2, result, name
258
+ request_compounds
259
+ end
260
+ end
261
+
262
+ def post_message_action
263
+ if input = @prompt_win.ask("Message (# msg)")
264
+ num, message = input.split(' ',2)
265
+ message num, message
266
+ read
267
+ end
268
+ end
269
+
270
+ def take_action
271
+ take
272
+ end
273
+
274
+ def action_loop
275
+ EM.defer(-> { getch },
276
+ -> c do
277
+ act c
278
+ action_loop
279
+ end)
280
+ end
281
+ end
282
+
283
+ class UI
284
+ def initialize(handler)
285
+ @h = handler
286
+ end
287
+ end
288
+
289
+ begin
290
+ # tell the terminal to enter curses mode
291
+ FFI::NCurses.initscr
292
+ FFI::NCurses.noecho
293
+ FFI::NCurses.halfdelay 1
294
+
295
+ FFI::NCurses.printw "Connecting.... to #{host}:#{port}\n"
296
+
297
+ # causes changes to be displayed on the screen - maybe this means that NCurses
298
+ # uses front and back buffers for drawing?
299
+ FFI::NCurses.refresh
300
+ FFI::NCurses.keypad FFI::NCurses.stdscr, true
301
+
302
+ start host, port
303
+ ensure
304
+ # returns back to regular console mode
305
+ FFI::NCurses.endwin
306
+ end
307
+