Some progress.
[rbot] / rpg / rpg.rb
1 # Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/)
2 #
3 # Little IRC game
4 #
5 # (c) 2006 Mark Kretschmann <markey@web.de>
6 # Licensed under GPL V2.
7
8
9 load '/home/eean/.rbot/plugins/rpg_creatures.rb'
10
11 class Map
12
13   attr_accessor :map, :legend
14
15   def initialize
16     @legend = { 'O' => Orc, 'S' => Slime, 's' => Sword }
17
18     # Maps are 16x16 
19     # X = player spawn 
20     str = <<-END
21 ----------------
22 | S |          |
23 |   |  |-----  |
24 |      |       |
25 |---|  |       |
26 |   |O |       |
27 |   |  --------|
28 |   |          |
29 |   | ----  |O |
30 |   |S|  |  |--|
31 |   | |  |     |
32 |---| |  |     |
33 |X s O|  |     |
34 |------  |     |
35 |        |     |
36 ----------------
37     END
38
39     @map = str.split( "\n")
40   end
41
42
43   def at( x, y )
44     @map[y][x].chr
45   end  
46
47
48   def wall?( x, y )
49     s = at( x, y)
50     s == '|' or s == '-'
51   end
52
53 end
54
55
56 class Objects < Array
57
58   def find_by_name( name )
59     object = nil
60     each do |o|
61       if o.name == name
62         object = o
63         break
64       end
65     end
66
67     return object
68   end
69
70 end
71
72
73 class Game
74
75   attr_accessor :channel, :objects, :map, :party_pos
76
77   def initialize( channel, bot )
78     @channel = channel
79     @bot = bot
80     @objects = Objects.new
81     @party_pos = Position.new
82
83     @map = Map.new
84     x, y = 0, 0
85     m = @map.map
86
87     # Read the map and spawn objects
88     m.length.times { |y|
89       m[y].length.times { |x| 
90         c = @map.at( x, y )
91         case c
92         when ' ', '-', '|'
93           next
94         when 'X'
95           @party_pos.x, @party_pos.y = x, y
96           next
97         else
98           o = spawn( @map.legend[c] )
99           o.pos.x, o.pos.y = x, y
100         end  
101       }
102     }
103
104   end
105
106
107   def set_players_pos( x, y )
108     debug( "set_players_pos(): #{x}  #{y}" )
109     @objects.each { |c| c.pos.x, c.pos.y = x, y if c.instance_of?( Player ) }
110   end
111      
112
113   def spawn( klass, name = nil )
114     o = klass.new
115     if name
116       o.name = name
117     end
118
119     objects << o
120     return o
121   end
122
123
124   def say( text )
125     @bot.say( @channel, text )
126   end  
127
128 end
129
130
131 class RpgPlugin < Plugin
132
133   def initialize
134     super
135
136     @games = Hash.new
137   end
138
139
140   def help( plugin, topic="" )
141     "IRC RPG. Commands: 'rpg', 'attack <target>', 'look [object]', 'take <object>', 'inventory', 'stats', 'go <north|n|east|e|south|s|west|w>'."
142   end
143
144 #####################################################################
145 # Core Methods
146 #####################################################################
147
148   # Returns new Game instance for channel, or existing one
149   #
150   def get_game( m )
151       channel = m.replyto
152
153       unless @games.has_key?( channel )
154           @games[channel] = Game.new( channel, @bot )
155       end
156
157       return @games[channel]
158   end
159
160
161   def schedule( g )
162     # Check for death:
163     g.objects.each do |p|
164       next unless p.kind_of?( Creature )
165       if p.hp < 0
166         g.say( "#{p.name} dies from his injuries." )
167         g.objects.delete( p )        
168       end  
169     end
170
171     # Let monsters act:
172     g.objects.each do |p|
173       if p.is_a?( Monster )
174         p.act( g )
175       end
176     end
177   end
178
179
180   def spawned?( g, nick )
181     if g.objects.find_by_name( nick )
182       return true
183     else
184       g.say( "You have not joined the game. Use 'rpg' to join." )
185       return false  
186     end
187   end
188
189
190   def target_spawned?( g, target )
191     if g.objects.find_by_name( target )
192       return true
193     else  
194       g.say( "There is noone named #{target} near.." )
195       return false
196     end
197   end
198
199
200   # Returns an array of objects at the same coordinates as p
201   def objects_near( g, p )
202     objects = []
203     g.objects.each { |o| objects << o if (o.pos == p.pos and o != p) }
204     return objects
205   end
206
207 #####################################################################
208 # Command Handlers
209 #####################################################################
210
211   def handle_rpg( m, params )
212     g = get_game( m )
213
214     o = g.spawn( Player, m.sourcenick )
215     o.pos.x, o.pos.y = g.party_pos.x, g.party_pos.y
216     m.reply "Player #{o.name} enters the game."
217   end
218
219
220   def handle_spawn_monster( m, params )
221     g = get_game( m )
222
223     o = g.spawn( Monster.monsters[rand( Monster.monsters.length )] ) 
224     o.pos.x, o.pos.y = g.party_pos.x, g.party_pos.y
225     m.reply "A #{o.object_type} enters the game. ('#{o.name}')"
226   end
227
228
229   def handle_attack( m, params )
230     g = get_game( m )
231     return unless spawned?( g, m.sourcenick )
232     return unless target_spawned?( g, params[:target] )
233  
234     g.objects[m.sourcenick].attack( g, g.objects[params[:target]] )
235     schedule( g )
236   end
237
238
239   def handle_look( m, params )
240     g = get_game( m )
241     return unless spawned?( g, m.sourcenick )
242
243     p = g.objects.find_by_name( m.sourcenick )
244     x, y = p.pos.x, p.pos.y
245     near = objects_near( g, p )
246
247     if params[:object] == nil
248       if near.empty?
249         m.reply( "#{m.sourcenick}: You are alone." )
250       else
251         names = []
252         near.each { |o| names << o.object_type }
253         m.reply( "#{m.sourcenick}: You see the following objects: #{names.join( ', ' )}." )
254       end
255
256       debug "MAP_LENGTH:  #{g.map.map.length}"
257       debug "PARTY_POS:   x:#{g.party_pos.x}  y:#{g.party_pos.y}"
258       debug "PLAYER_POS:  x:#{x}  y:#{y}"
259       debug "MAP NORTH: #{g.map.at( x, y-1 )}"
260
261       north = g.map.wall?( x, y-1 ) ? "a wall" : "open space"
262       east  = g.map.wall?( x+1, y ) ? "a wall" : "open space"
263       south = g.map.wall?( x, y+1 ) ? "a wall" : "open space"
264       west  = g.map.wall?( x-1, y ) ? "a wall" : "open space"
265
266       m.reply( "In the north is #{north}, east is #{east}, south is #{south}, and in the west you see #{west}." )
267     else
268       p = nil
269       near.each do |foo|
270         if foo.object_type.downcase == params[:object].downcase
271           p = foo
272           break
273         end        
274       end
275       if p
276         m.reply( "#{m.sourcenick}: #{p.description}" )   
277       else
278         m.reply( "#{m.sourcenick}: There is no #{params[:object]} here." )
279       end
280     end  
281   end
282
283
284   def handle_go( m, params )
285     g = get_game( m )
286     return unless spawned?( g, m.sourcenick )
287
288     wall = "Ouch! You bump into a wall."
289     x, y = g.party_pos.x, g.party_pos.y
290             
291     case params[:direction]
292       when 'north', 'n'
293         if g.map.wall?( x, y-1 )
294           m.reply wall 
295         else
296           g.party_pos.y -= 1
297           str = "You walk northward."
298         end
299       when 'east', 'e'
300         if g.map.wall?( x+1, y )
301           m.reply wall
302         else
303           g.party_pos.x += 1
304           str = "You walk eastward."
305        end     
306       when 'south', 's'
307         if g.map.wall?( x, y+1 )
308           m.reply wall
309         else
310           g.party_pos.y += 1  
311           str = "You walk southward."
312         end
313       when 'west', 'w'
314         if g.map.wall?( x-1, y )
315           m.reply wall
316         else
317           g.party_pos.x -= 1
318           str = "You walk westward."
319         end    
320      else
321         m.reply( "Go where? Directions: north, east, south, west." )
322         return
323     end    
324
325     x, y = g.party_pos.x, g.party_pos.y
326     g.set_players_pos( x, y )
327
328     exits = []
329     exits << "north" unless g.map.wall?( x, y-1 )
330     exits << "east"  unless g.map.wall?( x+1, y )
331     exits << "south" unless g.map.wall?( x, y+1 )
332     exits << "west"  unless g.map.wall?( x-1, y )
333     str += " (Exits: #{exits.join(', ')})"
334     m.reply( str )
335
336     p = g.objects.find_by_name m.sourcenick
337     near = objects_near( g, p )
338
339     unless near.empty?
340       near.each do |o|
341         m.reply "You encounter a #{o.object_type}!"
342       end
343     end
344   end
345
346
347   def handle_stats( m, params )
348     g = get_game( m )
349     return unless spawned?( g, m.sourcenick )
350
351     p = g.objects[m.sourcenick]
352     m.reply( "Stats for #{m.sourcenick}: HP:#{p.hp}  XP:#{p.xp}  THAC0:#{p.thac0}  AC:#{p.ac}  HD:#{p.hd}" )
353   end
354
355
356   def handle_take( m, params )
357     g = get_game( m )
358     return unless spawned?( g, m.sourcenick )
359     
360     p = g.objects.find_by_name m.sourcenick
361     near = objects_near( g, p )
362
363     t = nil
364     near.each do |foo|
365       if foo.object_type.downcase == params[:object].downcase
366         t = foo
367         break
368       end        
369     end
370
371     if t == nil
372       m.reply "#{m.sourcenick}: There is no #{params[:object]} here."
373       return
374     end
375
376     if t.kind_of?( Creature )
377       m.reply "#{m.sourcenick}: Feeling lonely, eh? You can't take persons."
378       return
379     end
380
381     t.pos.x, t.pos.y = nil, nil
382     p.inventory << t
383     m.reply "#{m.sourcenick} picks up a #{t.object_type}."
384   end
385
386
387   def handle_inventory( m, params )
388     g = get_game( m )
389     return unless spawned?( g, m.sourcenick )
390     p = g.objects.find_by_name m.sourcenick
391
392     if p.inventory.empty?
393       m.reply "#{m.sourcenick}: You don't carry any objects."
394     else
395       stuff = []
396       p.inventory.each { |i| stuff << i.object_type } 
397       m.reply "#{m.sourcenick}: You carry: #{stuff.join(' ')}"
398     end
399   end
400
401 end
402   
403
404 plugin = RpgPlugin.new
405 plugin.register( "rpg" )
406
407 plugin.map 'rpg',            :action => 'handle_rpg'
408 plugin.map 'spawn monster',  :action => 'handle_spawn_monster' 
409 plugin.map 'attack :target', :action => 'handle_attack' 
410 plugin.map 'look :object',   :action => 'handle_look',         :defaults => { :object => nil }
411 plugin.map 'go :direction',  :action => 'handle_go' 
412 plugin.map 'take :object',   :action => 'handle_take'
413 plugin.map 'stats',          :action => 'handle_stats'
414 plugin.map 'inventory',      :action => 'handle_inventory'
415