alchemist-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+