1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
#!/usr/bin/env python2
#author:stahlhoden
#project:rpg irc bot
#license: none

import sys
import socket
import string
import re
import random
import time

# ######################################################################################################### #
# This is an irc bot to help manage roleplaying with strangers                                              #
#                                                                                                           #
# I wrote a comment                                                                                         #
# http://www.reddit.com/r/rpg/comments/vgouy/role_play_with_a_stranger_want_to_make_it_happen/c54hjc4       #
#                                                                                                           #
# Also see                                                                                                  #
# http://www.reddit.com/r/randomrpgsite/                                                                    #
# http://www.reddit.com/r/rpg/comments/vgouy/role_play_with_a_stranger_want_to_make_it_happen/              #
# http://www.reddit.com/r/rpg/comments/vearv/role_play_with_a_stranger_yes_i_did/                           #
#                                                                                                           #
# Once this bot joins MAIN_CHANNEL it reacts to                                                             #
# !help - private messages some help text                                                                   #
# !showgames - private messages a list of games if available                                                #
# !newgame <num> <genre> - creates a new game where <num> is the number of desired players and <genre>      #
#     is a string telling about the genre (cannot have whitespaces).                                        #
#     when a new game is started the bot enters a random channel and informs the gm about the name of said  #
#     channel. if the channel has 0 players and is idle for >1minute the game gets deleted.                 #
#     as long as a game is not deleted the gm of this game can't create a new game                          #
#                                                                                                           #
# inside a gamechannel it reacts to                                                                         #
# !roll <dicenot> - public diceroll with common dicenotation like 3d6+6 or 1d20                             #
# !gmroll <dicenot> - secret diceroll for the gm                                                            #
# !unmute <name> - unmutes muted players. if a game is full (has like 9/9players) players can still join    #
#     the channel but they cannot talk. if the gm wants he can !unmute thisdude                             #
#                                                                                                           #
# DISCLAIMER: the code may not be good                                                                      #
# ######################################################################################################### # 


# globals

IRC_SERVER = "chat.freenode.net"
IRC_PORT = 6667
NICK = "RPG-SESSION-BOT"
FIRST_NAME = "Type"
LAST_NAME = "!help"
MAIN_CHANNEL = "#r.rpg"

USER_COMMANDS = ("PRIVMSG","JOIN","PART","QUIT")


# missing: prevent newgame spamming in a better way see _create_game method -> gm_list
# missing: rejoin on disconnect

def main():
	s=socket.socket( )
	s.connect((IRC_SERVER, IRC_PORT))
	s.send("NICK %s\r\n" % NICK)
	s.send("USER %s 8 * : %s %s\r\n" % (NICK, FIRST_NAME, LAST_NAME))
	s.send("JOIN %s\r\n" % (MAIN_CHANNEL))
	readbuffer = ""
	
	#thats the one
	gmb = GameManagerBot(s)
	
	last_time = time.time()
	while True:
		#read from irc
		readbuffer=readbuffer+s.recv(1024)
		temp=string.split(readbuffer, "\n")
		readbuffer=temp.pop()
		
		# cleanup old games
		timedelta = time.time()-last_time
		if timedelta >= 30:
			current_time = time.time()
			gmb.cleanup(current_time)
			last_time = current_time
			
		# forward lines to gamemanager or pong
		for line in temp:
			line=string.rstrip(line)
			print line

			if "PING" in line:
				pong(line, s)
			elif is_user_command(line):
				gmb.next_line(line)

def pong(line, socket):
	"""keepalive pong back"""
	if line.find("PING") == 0:
		socket.send("PONG %s\r\n" % string.split(line)[1])
		
def is_user_command(line):
	"""determines if line was a PRIVMSG, JOIN, PART... and not some server stuff"""
	tokens = string.split(line)
	if len(tokens) > 2:
		for command in USER_COMMANDS:
			if command == tokens[1]: 
				return True
	else:
		return False
		
def send_message(socket,to,message):
	"""generic send_message command template"""
	if len(message) > 350:
		message = "cannot compute :("
	socket.send("PRIVMSG %s :%s\r\n" % (to,message))


class GameManagerBot:
	"""bot manages the game sessions"""
	_game_sessions = {}
	_gm_list = []
	
	#regex
	_game_request_re = re.compile(r":(\S+)!(\S+)\sPRIVMSG\s%s\s:!newgame\s(\d+)\s(\S+)" % MAIN_CHANNEL)
	_show_games_re = re.compile(r":(\S+)!(\S+)\sPRIVMSG\s%s\s:!showgames" % MAIN_CHANNEL)
	_help_re = re.compile(r":(\S+)!(\S+)\sPRIVMSG\s%s\s:!help" % MAIN_CHANNEL)
	_quit_re = re.compile(r":(\S+)!(\S+)\sQUIT")
	
	def __init__(self, socket):
		self.socket=socket

	def next_line(self,line):
		"""eats new lines and delegates them to where they belong"""
		# notify all games on generic quit
		match = self._quit_re.match(line)
		if match:
			name = match.group(1)
			for channel in self._game_sessions:
				self._game_sessions[channel].user_quit(name)
			return

		# pass lines to where they belong
		channel = self._get_channel(line)		
		if MAIN_CHANNEL == channel:
			self._parse_main_channel_line(line)
		elif channel in self._game_sessions:
			self._game_sessions[channel].next_line(line)
			
	def cleanup(self,current_time):
		"""deletes games that are empty and idle for more than a minute"""
		dead_channels=[]
		for channel in self._game_sessions:
			game = self._game_sessions[channel]
			timedelta = current_time - game.last_activity
			if game.player_count == 0 and timedelta > 60:
				dead_channels.append(channel)
				send_message(self.socket,game.gm_nick,"Your game %s will get deleted because it was empty and idle"%channel)
		for channel in dead_channels:
			print "REMOVING GAME %s"%channel
			self._remove_game(channel)
		
			
			
	def _parse_main_channel_line(self,line):
		"""parses lines from the main channel"""
		#new game?
		match = self._game_request_re.match(line)
		if match:
			self._create_game(match)
			return
		
		# show games?
		match = self._show_games_re.match(line)
		if match:
			self._show_games(match)
			return
		
		# need help?
		match = self._help_re.match(line)
		if match:
			user = match.group(1)
			send_message(self.socket,user,"Hi there, this bot is simple. Use !showgames to see channels with games in it")
			send_message(self.socket,user,"Use !newgame <max_players> <genre> to become gm of a new game like !newgame 4 scifi")
			send_message(self.socket,user,"Once in a gamechannel you can use the !roll command, like !roll 3d6+10 if you're gm you cam !gmroll too")
			
			
	def _show_games(self,match):
		"""sends a list of games to the guy who typed !showgames"""
		user=match.group(1)
		
		if self._game_sessions == {}:
			send_message(self.socket,user,"There are no games currently running. You can GM one yourself. Type !newgame <num_players> <genre> | Example: !newgame 6 western")
			return
		
		current_time = time.time()		
		for gamechannel in self._game_sessions:
			gs=self._game_sessions[gamechannel]
			timedelta = (current_time - gs.last_activity)/60
			message = "Game by %s with genre '%s' | %d/%d players in channel %s | idle since %dmin" % (gs.gm_nick,gs.genre,gs.player_count,gs.num_players,gs.channel,timedelta)
			send_message(self.socket,user,message)
	
	
	def _create_game(self,match):
		"""creates a new game and sends the info to the gm"""
		gm_nick = match.group(1)
		gm_host = match.group(2)
		num_players = match.group(3)
		genre = match.group(4)
		channel = self._random_unused_channel()

		# trying to avoid spamming of newgame by restricting to 1 game per user. not good enough
		if gm_nick in self._gm_list:
			send_message(self.socket,gm_nick,"You can't create a new game because you are already GM of another game. If you just left a previous game that channel must remain empty and idle for a while to get deleted")
			return
		self._gm_list.append(gm_nick)
		
		game_session = GameSessionBot(self,channel,gm_nick,gm_host,int(num_players),genre)
		self.socket.send("JOIN %s\r\n" % channel)
		self._game_sessions[channel] = game_session

		message = "Hello GM %s please join %s for your game." % (gm_nick,channel)
		send_message(self.socket,gm_nick,message)
		
		game_session.set_topic()
		
	def _remove_game(self,channel):
		"""parts from channel and removes game from dict"""
		self.socket.send("PART %s\r\n"%channel)
		self._gm_list.remove(self._game_sessions[channel].gm_nick)
		del self._game_sessions[channel]
		
	def _get_channel(self,line):
		"""parses the channel name from a line"""
		return string.split(line)[2]
		
	def _random_unused_channel(self):
		"""finds a random channelname that isn't used for a game yet"""
		channel = self._random_channel()
		while channel in self._game_sessions:
			channel = self._random_channel()
		return channel
		
	def _random_channel(self):
		"""generates a random channel name"""
		return "%s_game_%d" % (MAIN_CHANNEL, random.randint(1000000,9999999))
	


class GameSessionBot:
	"""bot is responsible for a single game"""
	
	def __init__(self,game_manager,channel,gm_nick,gm_host,num_players,genre):
		self.game_manager=game_manager
		self.socket=game_manager.socket
		self.channel=channel
		self.gm_nick=gm_nick
		self.gm_host=gm_host
		self.num_players=num_players
		self.genre=genre
		self.player_count=-1 #-1 because bot will count itself on joining
		self.player_names=[]
		self.last_activity=time.time();
		
		#regex		
		self._roll_re = re.compile(r":(\S+)!(\S+)\sPRIVMSG\s%s\s:!roll\s(\d+)d(\d+)((\+|-)(\d+))?" % self.channel)
		self._gm_roll_re = re.compile(r":(\S+)!(\S+)\sPRIVMSG\s%s\s:!gmroll\s(\d+)d(\d+)((\+|-)(\d+))?" % self.channel)
		self._unmute_re = re.compile(r":(\S+)!(\S+)\sPRIVMSG\s%s\s:!unmute\s(\S+)" % self.channel)
		self._join_re = re.compile(r":(\S+)!(\S+)\sJOIN")
		self._part_re = re.compile(r":(\S+)!(\S+)\sPART")
		
	def next_line(self,line):
		"""parses lines from the channel this game belongs to"""
		self.last_activity=time.time()

		# did someone roll?
		match = self._roll_re.match(line)
		if match:
			self._user_roll(match)
			return
		
		# did gm roll?
		match = self._gm_roll_re.match(line)
		if match:
			self._gm_roll(match)
			return
		
		# new player?
		match = self._join_re.match(line)
		if match:
			nick = match.group(1)
			self._add_player(nick)
			return
		
		#player left?
		match = self._part_re.match(line)
		if match:
			nick = match.group(1)
			self._remove_player(nick)
			return
			
		#unmute
		match = self._unmute_re.match(line)
		if match:
			self._unmute_by_command(match)
			return
			
	def set_topic(self):
		message = "Welcome to the game. Your GM is %s and the genre is %s. Rolling works with standard dice notation | Example !roll 3d6+3 | The gm can !gmroll and !unmute"%(self.gm_nick,self.genre)
		self.socket.send("TOPIC %s :%s\r\n"%(self.channel,message))
		self.socket.send("MODE %s +m\r\n"%self.channel)
			
	def user_quit(self,user):
		"""checks if quiting user belonged to this games and deletes him if so"""
		if user in self.player_names:
			self._remove_player(user)
		
	def _add_player(self,name):
		"""increases player count"""
		self.player_count = self.player_count + 1
		self.player_names.append(name)
		
		# mute players above numplayers
		if self.player_count > self.num_players:
			self._mute(name)
		else:
			self._unmute(name)
			
	def _unmute_by_command(self,match):
		issued_by = match.group(1)
		unmute_who = match.group(3)
		if issued_by == self.gm_nick:
			self._unmute(unmute_who)
	
		
	def _mute(self,player):
		self.socket.send("MODE %s -v %s\r\n" % (self.channel,player))
	
	def _unmute(self,player):
		self.socket.send("MODE %s +v %s\r\n" % (self.channel,player))
	
	def _remove_player(self,name):
		"""decreases player count"""
		self.player_count = self.player_count - 1
		self.player_names.remove(name)
		
	def _user_roll(self,match):
		"""sends result of roll to channel"""
		message = "%s rolled \x02%s\x02" % (match.group(1),self._roll_from_match(match))
		send_message(self.socket,self.channel,message)
		
	def _gm_roll(self,match):
		"""sends result of roll to gm"""
		user = match.group(1)
		if self.gm_nick == user:
			message = "You rolled is \x02%s\x02" % (self._roll_from_match(match))
			send_message(self.socket,self.gm_nick,message)
		else:
			send_message(self.socket,user,"You can't !gmroll in %s"%self.channel)

	def _roll_from_match(self,match):
		"""reads values from roll regex and returns roll string """
		num_dice = int(match.group(3))
		die_sides = int(match.group(4))
		mod = 0
		if match.group(5):
			mod = int(match.group(5))
		return self._roll(num_dice,die_sides,mod)
	
	def _roll(self,num_dice,die_sides,mod):
		"""build roll string from dicevalues"""
		
		#sanity check
		if num_dice > 100 or num_dice < 1:
			return "a stupid amount of dice"
		if die_sides > 999 or die_sides < 1:
			return "some ugly dice"
			
		dice = []
		result = 0
		for i in range(num_dice):
			die = random.randint(1,die_sides)
			dice.append(die)
			result = result + die
			
		result_string = "[ %s ]" % "-".join("%s" % d for d in dice)
		result = result + mod
		
		if mod == 0:
			return "%s %s" % (result_string," = %d" % result)
		elif mod < 0:
			return "%s %s" % (result_string," - %d = %d" % (-mod,result))
		else:
			return "%s %s" % (result_string," + %d = %d" % (mod,result))
		

if __name__ == "__main__":
	sys.exit(main())