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,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
|
+
|