Exemplo n.º 1
0
class CanastaServer(pb.Root):
    def __init__(self,controller_id):
	self.g = CanastaRound(images=False)
	self.playerlist = {}
	self.idlist = []
	self.players = []
	self.accept_clients = True
	self.game_started = False
	self.players_ready = [False]*4
	self.wait_response = False
	self.pause = False
	self.executing = False
	self.computing = False
	self.controller_id = UUID(controller_id)
	self.shut_down = False
	self.last_command_time = 0
	if DEBUGGER: print "server started"

    def remote_debug(self):
	self.g.roundOver=True
	for player in self.playerlist:
	    player[2].callRemote("debug")

    def remote_Reset(self,id):
	if self.playerlist[str(id)][1]:
	    self.gameReset()


    def infoChat(self,message):
	"""
	Send a server informational message to the chat windows of the clients
	"""
	command = CanastaCommand(CHAT,[message],[INFO_STRING])
	self.sendChat(command)

    def tryGoOut(self,id):
	"""
	Query the current player's partner to ask whether the player can go out.
	To prevent table talk, chatting (and all other command execution) is blocked until the partner
	responds.
	"""

	if self.g.canGoOut():
	    message = self.playerlist[str(id)][0] + " asked to go out"
	    self.infoChat(message)
	    opp_pos = self.playerset.index(str(id)) + 2
	    if opp_pos > 3:
		opp_pos -= 4
	    opp_player = self.playerlist[self.playerset[opp_pos]]
	    self.wait_response = True
	    if isinstance(opp_player[2],ComputerPlayer):
		result = opp_player[2].mayIGoOut()
		self.goOutNow(result)
	    else:
		opp_player[2].callRemote("goOut").addCallbacks(self.goOutNow,self.goOutError)
	else:
	    return

    def goOutError(self,obj):
	if DEBUGGER: print ["error going out",obj]

    def goOutNow(self,obj):
	"""
	Executed after querying a partner about going out. If response is yes, meld any staged cards and
	discard to end the round. If response is no, send a special command that prevents the player from
	going out this turn.
	"""
	if obj:
	    self.infoChat("Partner said yes")
	    code1 = CanastaCommand(MELD_CARDS,[],[])
	    code2 = CanastaCommand(DISCARD_CARD,[1],[])
	    self.execCommand(code1)
	    self.execCommand(code2)
	else:
	    self.infoChat("Partner said no")
	    code = CanastaCommand(BLOCK_OUT,[],[])
	    self.execCommand(code)
	self.wait_response = False

    def replacePlayer(self,which,name):
	self.need_replace = which
	self.pause = True
	for id in self.idlist:
	    player = self.playerlist[str(id)][2]
	    print id,player
	    if self.playerlist[str(id)][1]:
		player.callRemote("lostPlayer",name).addCallback(self.gotReplacement)
	    else:
		player.callRemote("waitPlayer",name)

    def gotReplacement(self,obj):
	if obj:
	    p = ComputerPlayer(self.need_replace)
	    self.playerset[self.need_replace] = str(self.need_replace)
	    self.playerlist[str(self.need_replace)] = ["Computer " + str(self.need_replace),False,p]
	    print "replacing", self.need_replace,self.g.playernames
	    self.g.playernames[self.need_replace] = "Computer " + str(self.need_replace)
	    for p in self.idlist:
		player = self.playerlist[p]
		player[2].callRemote("updateNames",self.remote_Names())
	else:
	    self.playerset.remove(self.playerset[self.need_replace])
	    self.gameReset()
	for id in self.idlist:
	    self.playerlist[id][2].callRemote("unPause")
	self.pause = False

    def removeDeadClient(self,player,p):
	"""
	Removes disconnected clients from the server's records. If the dead client was an observer, continue on with the game. If they were a player, end the game and kick the other clients back to the player-assignment stage.
	"""
	pname = self.playerlist[str(p)][0]
	self.idlist.remove(p)
	self.players.remove(self.playerlist[str(p)][0])
	del self.playerlist[str(p)]
	if self.game_started:
	    if p in self.playerset:
		which = self.playerset.index(p)
		if DEBUGGER: print "Player dropped off, querying"
		self.replacePlayer(which,pname)
	    else:
		self.observerset.remove(p)
		if DEBUGGER: print "Observer dropped off, removing them"
	for p in self.idlist:
	    player = self.playerlist[p]
	    player[2].callRemote("removeName",pname)
	try: self.g.playernames[which] = None
	except: pass
	self.infoChat(pname + " has disconnected")

    def doneExecuting(self,obj=None):
	self.executing = False

    def execCommand(self,command):
	"""
	Excute a game command both locally and on the client players.

	If the round is over, update scores and prepare to wait for the clients.
	"""
	self.executing = True
	cur_turn = self.g.turn
	self.g.execCode(command)
	if DEBUGGER: print ["server executed",self.g.lastCommand,self.g.lastReturn]
	for turn, p in enumerate(self.playerset+self.observerset):
	    player = self.playerlist[str(p)][2]
	    if isinstance(player,ComputerPlayer):
		pass
	    elif self.g.lastReturn:
		try:
		    player.callRemote("readCommand",command).addCallbacks(self.sentCommand,self.errCommand)
		except:
		    self.removeDeadClient(player,p)
	if self.g.lastReturn:
	    self.updateComputers(cur_turn)
	if self.g.roundOver:
	    if self.g.lastReturn:
		if DEBUGGER: print "ROUND OVER *** server updating score"
		team1round = self.g.cardPoints(1) + self.g.specialPoints(1,params=False) - self.g.handPoints(1)
		team2round = self.g.cardPoints(2) + self.g.specialPoints(2,params=False) - self.g.handPoints(2)

		self.g.team1score += team1round
		self.g.team2score += team2round
		for turn, p in enumerate(self.playerset):
		    player = self.playerlist[str(p)][2]
		    if isinstance(player,ComputerPlayer):
			self.players_ready[turn] = player.isReady()
		    else:
			self.players_ready[turn] = False
	self.doneExecuting()

    def sendChat(self,command):
	"""
	Sends a chat message to all clients.
	"""
	self.g.execCode(command)
	if DEBUGGER: print [self.g.lastCommand,self.g.lastReturn]
	for p in self.idlist:
	    player = self.playerlist[str(p)][2]
	    if not isinstance(player,ComputerPlayer):
		player.callRemote("readCommand",command)

    def notClosed(self,obj):
	if DEBUGGER: print "Client did not close correctly:",obj
	self.is_closed = True

    def isClosed(self,obj):
	if DEBUGGER: print "Client closed correctly:",obj
	self.is_closed = True

    def gameReset(self):
	self.executing = True
	to_remove = []
	for id in self.playerset:
	    player = self.playerlist[str(id)][2]
	    if isinstance(player,ComputerPlayer):
		print self.playerlist,id,self.g.playernames
		self.g.playernames.remove(self.playerlist[str(id)][0])
		del self.playerlist[str(id)]
		to_remove.append(id)
	for id in to_remove: self.playerset.remove(id)
	for turn, p in enumerate(self.playerset+self.observerset):
	    player = self.playerlist[str(p)][2]   
	    player.callRemote("updateNames",self.remote_Names())
	    player.callRemote("resetGame")
	self.playerset = []
	self.observerset = []
	self.game_started = False
	self.executing = False

    def nextRound(self):
	"""
	Start the next round. Reset the scores if someone went over 5000 in the last round.
	"""
	if not(False in self.players_ready):
	    if DEBUGGER: print "initializing the next round"
	    if ((self.g.team1score>5000) | (self.g.team2score>5000)) & (self.g.team1score != self.g.team2score):
		self.gameReset()
	    else:
		self.initRound()
		self.initClients()

    def newGame(self):
	"""
	Tell the clients that a new game is starting, so they should reset their scores.
	"""
	self.g.newGame()
	for pos, p in enumerate(self.playerset+self.observerset):
	    player = self.playerlist[p][2]
	    if isinstance(player,ComputerPlayer):
		pass
	    else:
		if DEBUGGER: print "resetting the game"
		player.callRemote("newGame")

    def initGame(self,options):
	"""
	Send the clients the global game settings: the gameplay options, the client's position on the board, and the names of the players.
	"""
	for pos, p in enumerate(self.playerset+self.observerset):
	    player = self.playerlist[str(p)][2]
	    if isinstance(player,ComputerPlayer):
		pass
	    else:
		if DEBUGGER: print "initializing the client"
		player.callRemote("initGame",self.g.playernames,pos,options)

    def initRound(self):
	"""
	Initialize the local game object for the current round. This object will be kept in sync with the parallel objects that are held by the clients.
	"""
	self.g.initCanasta()
	self.g.dealRound()

    def initClients(self):
	"""
	Initialize the round by sending the clients an object describing the deal. After this is called, all game objects held by the server and the clients should be in an identical state.
	"""
	for pos, p in enumerate(self.playerset+self.observerset):
	    player = self.playerlist[p][2]
	    if p in self.observerset:
		player.callRemote("resetRound")
	    if isinstance(player,ComputerPlayer):
		player.initRound(self.g.initStatus())
	    else:
		if DEBUGGER: print "starting the client round"
		player.callRemote("initRound")
		if DEBUGGER: print "initializing the cards"
		status = self.g.initStatus()
		player.callRemote("readInit",status)
	self.players_ready = [False]*4



    def remote_joinServer(self, client, name, id, version):
	if not self.accept_clients:
	    return "This server is not accepting connections -- it is probably in one-player mode."
	elif version != VERSION:
	    return "Error: incompatible client, this host requires version "+VERSION[0]+"."+VERSION[1]+"."+VERSION[2]
	count = 1
	newname = name
	while newname in self.players:
	    newname = name + str(count)
	    count += 1
	name = newname
	if DEBUGGER: print ["player",name,"joined",id]
	if self.game_started:
	    while self.executing | self.computing:
		pass
	    self.executing = True
	    sleep(LOOP_RATE*2)
	    if DEBUGGER: print "Adding player as observer in position",len(self.observerset)+4
	    client.callRemote("initGame",self.g.playernames+[name],len(self.observerset)+4,self.g.options)
	    client.callRemote("initRound")
	    if DEBUGGER: print "initializing the cards"
	    status = self.g.initStatus()
	    self.observerset.append(id)
	    self.g.playernames.append(name)
	    client.callRemote("readInit",status)
	else:
	    self.g.playernames = [None]*4 + self.players + [name]
	self.playerlist[id] = [name,False,client]
	if UUID(id) == self.controller_id:
	    self.playerlist[id][1]=True
	self.players.append(name)
	self.idlist.append(id)
	for index, p in enumerate(self.idlist):
	    player = self.playerlist[p]
	    player[2].callRemote("updateNames",self.remote_Names())
	    if not self.game_started: player[2].callRemote("initGame",self.g.playernames,4+index,CanastaOptions())
	    else: pass
	self.infoChat(name+" has arrived.")
	client.callRemote("lookAlive").addCallback(self.doneExecuting)
	return self.playerlist[id][1]
    
    def remote_blockConnections(self):
	self.accept_clients = False

    def remote_hangUp(self,id):
	player = self.playerlist[id][2]
	self.removeDeadClient(player,id)

    def remote_takeCanastaCommand(self, id, command):
	"""
	The main command invoked by clients that want to execute commands.

	Chats should go through the back channel, but they will be redirected if they show up here.
	Game commands are allowed through only on the player's turn. (For efficiency, the client
	is programmed to only send commands on its turn). 

	GO_OUT is a special command, which cannot be executed by the game engine (it will give an error
	if it is passed.) It is trapped by the server, which then tests whether the client can go out, and
	if so it queries the partner. All commands and chats are blocked if the server is waiting on a 
	request to go out.
	"""
	if self.pause:
	    return
	self.last_command_time = 0
	client = self.playerlist[str(id)][2]
	if command.action == GO_OUT:
	    if DEBUGGER: print "got go-out ask"
	    self.tryGoOut(id)
	elif (not self.wait_response):
	    if command.action == CHAT:
		self.sendChat(command)
	    elif self.g.turn == self.playerset.index(str(id)):
		self.execCommand(command)

    def sentCommand(self,obj):
	if DEBUGGER: print "successfully sent command",obj

    def errCommand(self,obj):
	if DEBUGGER: print "error sending command",obj

    def remote_takeChat(self, id, command):
	self.last_command_time = 0
	"""
	Back channel for sending chat messages, which does not check turn status.
	Blocked if the server is waiting on a request to go out.
	"""
	client = self.playerlist[str(id)][2]
	if (command.action == CHAT) & (not self.wait_response):
	    self.sendChat(command)

    def remote_Names(self):
	namelist = []
	if self.game_started:
	    ids = self.playerset + self.observerset
	else:
	    ids = self.idlist
	for i in ids:
	    namelist.append(self.playerlist[i][0])
	return namelist

    def remote_assignPlayers(self,id,poslist):
	self.all_computers = True
	if self.playerlist[str(id)][1]:
	    self.playerset = []
	    self.observerset = []
	    names = []
	    positions = [None]*4
	    for index, p in enumerate(poslist):
		try:
		    positions[index] = self.idlist[self.players.index(p)]
		except:
		    pass
	    for index, p in enumerate(positions):
		if p==None:
		    cur_p = ComputerPlayer(index)
		    self.playerset.append(str(index))
		    self.playerlist[str(index)] = ["Computer " + str(index),False,cur_p]
		    names.append("Computer " + str(index))
		else:
		    self.all_computers = False
		    self.playerset.append(p)
		    names.append(poslist[index])
	    for i in self.idlist:
		if i not in positions: 
		    self.observerset.append(i)
		    names.append(self.playerlist[str(i)][0])
	return names

    def remote_Start(self,id,poslist,options):
	"""
	Start a game and initialize all the player positions, inserting computers where no human player
	is specified.
	This function only accepts commands from the controlling client, which is the first client to connect to the server. It is intended that this will always be the local player whose application has launched the
	server as its subprocess.
	"""

	if self.playerlist[str(id)][1]:
	    names = self.remote_assignPlayers(id,poslist)
	    if self.all_computers: 
		options.animation=2000
	    else:
		options.animation=None
	    self.g.gameStart(names,-1,options)
	    print "NAMELIST:",self.g.playernames
	    self.initGame(options)
	    self.initRound()
	    self.initClients()
	    self.game_started = True
	    self.infoChat("Game on!")

    def remote_isReady(self,id):
	"""
	Called by clients between rounds, to indicate that they are ready to start the next round. The server won't initialize the round until every client gives the 'ready' signal.
	"""
	if DEBUGGER: print "got ready",id
	client = self.playerlist[str(id)][2]
	self.players_ready[self.playerset.index(str(id))] = True
	if DEBUGGER: print self.players_ready	

    def remote_Shutdown(self,id):
	client = self.playerlist[str(id)][2]
	controller = self.playerlist[str(id)][1]
	players = self.idlist
	if controller:
	    self.executing = True
	    for p in players:
		player = self.playerlist[str(p)][2]
		controller = self.playerlist[str(p)][1]
		name = self.playerlist[str(p)][0]
		if not isinstance(player,ComputerPlayer):
		    if not controller:
			self.is_closed = False
			if DEBUGGER: print ["killing player",name]
			try:
			    player.callRemote("endGame").addCallbacks(self.isClosed,self.notClosed)
			except:
			    self.notClosed("Dead Client")
			while not self.is_closed:
			    reactor.iterate()
	    self.shut_down = True


    def updateComputers(self,cur_turn):
	"""
	Update the computer players on the results of the last play.

	If this is called more than once between commands, it will result in memory errors in the computer
	players.
	"""
	for turn, p in enumerate(self.playerset):
	    player = self.playerlist[str(p)][2]
	    if isinstance(player,ComputerPlayer):
		if DEBUGGER: print ["updating computer",turn]
		if cur_turn==turn:
		    state = self.g.curState(active=True)
		else:
		    state = self.g.curState(active=False)
		player.readPlay(state)

    def playLocals(self):
	"""
	The server's main computation loop, which runs concurrently with the Twisted reactor loop.

	It continually checks whether it's a computer player's turn, and if so it solicits a command.
	"""

	if self.shut_down:
	    reactor.stop()
	elif self.game_started & (not self.executing) & (not self.g.roundOver) & (not self.pause):
	    self.computing = True
	    if isinstance(self.playerlist[self.playerset[self.g.turn]][2],ComputerPlayer) & (not self.g.roundOver):
		cur_turn = self.g.turn
		play = self.playerlist[self.playerset[self.g.turn]][2].getPlay(self.g.curState())
		if play.action == CHAT:
		    self.sendChat(play)
		else:
		    self.execCommand(play)
	    else:
		self.last_command_time += 1
		if self.last_command_time > TIMEOUT/LOOP_RATE:
		    self.last_command_time = 0
		    for turn, p in enumerate(self.playerset+self.observerset):
			player = self.playerlist[str(p)][2]   	
			if not isinstance(player,ComputerPlayer):
			    try:
				player.callRemote("lookAlive")
			    except:
				self.removeDeadClient(player,p)
	    self.computing = False
	elif self.g.roundOver:
	    self.nextRound()