564 lines
24 KiB
Python
564 lines
24 KiB
Python
#################################################################
|
|
# class: BingoManagerAI.py
|
|
#
|
|
# Purpose: Manages the Bingo Night Holiday for all ponds in all
|
|
# hoods. It generates PondBingoManagerAI objects for
|
|
# every pond and shuts them down respectively. In
|
|
# addition, it should handle all Stat collection such
|
|
# as top jackpot of the night, top bingo players, and
|
|
# so forth.
|
|
#
|
|
# Note: Eventually, this will derive from the HolidayBase class
|
|
# and run each and ever Bingo Night, whenever that has
|
|
# been decided upon.
|
|
#################################################################
|
|
|
|
#################################################################
|
|
# Direct Specific Modules
|
|
#################################################################
|
|
from direct.distributed import DistributedObjectAI
|
|
from direct.distributed.ClockDelta import *
|
|
from direct.directnotify import DirectNotifyGlobal
|
|
from otp.otpbase import PythonUtil
|
|
from direct.task import Task
|
|
|
|
#################################################################
|
|
# Toontown Specific Modules
|
|
#################################################################
|
|
from toontown.estate import DistributedEstateAI
|
|
from toontown.fishing import BingoGlobals
|
|
from toontown.fishing import DistributedFishingPondAI
|
|
from toontown.fishing import DistributedPondBingoManagerAI
|
|
from direct.showbase import RandomNumGen
|
|
from toontown.toonbase import ToontownGlobals
|
|
from toontown.hood import ZoneUtil
|
|
|
|
#################################################################
|
|
# Python Specific Modules
|
|
#################################################################
|
|
import pickle
|
|
import os
|
|
import time
|
|
|
|
#################################################################
|
|
# Globals and Constants
|
|
#################################################################
|
|
TTG = ToontownGlobals
|
|
BG = BingoGlobals
|
|
|
|
class BingoManagerAI(object):
|
|
# __metaclass__ = PythonUtil.Singleton
|
|
notify = DirectNotifyGlobal.directNotify.newCategory("BingoManagerAI")
|
|
#notify.setDebug(True)
|
|
#notify.setInfo(True)
|
|
serverDataFolder = simbase.config.GetString('server-data-folder', "dependencies/backups/bingo")
|
|
|
|
DefaultReward = { TTG.DonaldsDock: [BG.MIN_SUPER_JACKPOT, 1],
|
|
TTG.ToontownCentral: [BG.MIN_SUPER_JACKPOT, 1],
|
|
TTG.TheBrrrgh: [BG.MIN_SUPER_JACKPOT, 1],
|
|
TTG.MinniesMelodyland: [BG.MIN_SUPER_JACKPOT, 1],
|
|
TTG.DaisyGardens: [BG.MIN_SUPER_JACKPOT, 1],
|
|
TTG.DonaldsDreamland: [BG.MIN_SUPER_JACKPOT, 1],
|
|
TTG.MyEstate: [BG.MIN_SUPER_JACKPOT, 1] }
|
|
|
|
############################################################
|
|
# Method: __init__
|
|
# Purpose: This method initializes the BingoManagerAI object
|
|
# and generates the PondBingoManagerAI.
|
|
# Input: air - The AI Repository.
|
|
# Output: None
|
|
############################################################
|
|
def __init__(self, air):
|
|
self.air = air
|
|
|
|
# Dictionaries for quick reference to the DPMAI
|
|
self.doId2do = {}
|
|
self.zoneId2do = {}
|
|
self.hood2doIdList = { TTG.DonaldsDock: [],
|
|
TTG.ToontownCentral: [],
|
|
TTG.TheBrrrgh: [],
|
|
TTG.MinniesMelodyland: [],
|
|
TTG.DaisyGardens: [],
|
|
TTG.DonaldsDreamland: [],
|
|
TTG.MyEstate: [] }
|
|
|
|
self.__hoodJackpots = {}
|
|
self.finalGame = BG.NORMAL_GAME
|
|
self.shard = str(air.districtId)
|
|
self.waitTaskName = 'waitForIntermission'
|
|
|
|
# Generate the Pond Bingo Managers
|
|
self.generateBingoManagers()
|
|
|
|
############################################################
|
|
# Method: start
|
|
# Purpose: This method "starts" each PondBingoManager for
|
|
# the Bingo Night Holidy.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def start(self):
|
|
# Iterate through keys and change into "active" state
|
|
# for the pondBingoManagerAI
|
|
self.notify.info("Starting Bingo Night Event: %s" % (time.ctime()))
|
|
self.air.bingoMgr = self
|
|
# Determine current time so that we can gracefully handle
|
|
# an AI crash or reboot during Bingo Night.
|
|
currentMin = time.localtime()[4]
|
|
self.timeStamp = globalClockDelta.getRealNetworkTime()
|
|
initState = ((currentMin < BG.HOUR_BREAK_MIN) and ['Intro'] or ['Intermission'])[0]
|
|
|
|
# CHEATS
|
|
#initState = 'Intermission'
|
|
for do in list(self.doId2do.values()):
|
|
do.startup(initState)
|
|
self.waitForIntermission()
|
|
|
|
# tell everyone bingo night is starting
|
|
simbase.air.newsManager.bingoStart()
|
|
|
|
############################################################
|
|
# Method: stop
|
|
# Purpose: This method begins the process of shutting down
|
|
# bingo night. It is called whenever the
|
|
# BingoNightHolidayAI is told to close for the
|
|
# evening.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def stop(self):
|
|
self.__startCloseEvent()
|
|
|
|
############################################################
|
|
# Method: __shutdown
|
|
# Purpose: This method performs the actual shutdown sequence
|
|
# for the pond bingo manager. By this point, all
|
|
# of the PondBingoManagerAIs should have shutdown
|
|
# so we can safely close.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def shutdown(self):
|
|
self.notify.info('__shutdown: Shutting down BingoManager')
|
|
|
|
# tell everyone bingo night is stopping
|
|
simbase.air.newsManager.bingoEnd()
|
|
|
|
if self.doId2do:
|
|
#self.notify.warning('__shutdown: Not all PondBingoManagers have shutdown! Manual Shutdown for Memory sake.')
|
|
for bingoMgr in list(self.doId2do.values()):
|
|
self.notify.info("__shutdown: shutting down PondBinfoManagerAI in zone %s" % bingoMgr.zoneId)
|
|
bingoMgr.shutdown()
|
|
self.doId2do.clear()
|
|
del self.doId2do
|
|
|
|
self.air.bingoMgr = None
|
|
del self.air
|
|
del self.__hoodJackpots
|
|
|
|
############################################################
|
|
# Method: __resumeBingoNight
|
|
# Purpose: This method resumes Bingo Night after an
|
|
# an intermission has taken place. This should
|
|
# start on the hour.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def __resumeBingoNight(self, task):
|
|
self.__hoodJackpots = self.load()
|
|
for bingoMgr in list(self.doId2do.values()):
|
|
if bingoMgr.isGenerated():
|
|
if self.finalGame:
|
|
bingoMgr.setFinalGame(self.finalGame)
|
|
bingoMgr.resumeBingoNight()
|
|
timeToWait = BG.getGameTime(BG.BLOCKOUT_CARD) + BG.TIMEOUT_SESSION + 5.0
|
|
taskMgr.doMethodLater(timeToWait, self.__handleSuperBingoClose, 'SuperBingoClose')
|
|
|
|
# If we have another game after this, then do not want to generate a
|
|
# new task to wait for the next intermission.
|
|
return Task.done
|
|
|
|
############################################################
|
|
# Method: __handleSuperBingoClose
|
|
# Purpose: This method is responsible for logging the
|
|
# current hood jackpot amounts to the .jackpot
|
|
# "database" file. In addition, it initiates the
|
|
# shutdown of the BingoManagerAI if the final
|
|
# game of the evening has been played.
|
|
# Input: task - a task that is spawned by a doMethodLater
|
|
# Output: None
|
|
############################################################
|
|
def __handleSuperBingoClose(self, task):
|
|
# Save Jackpot Data to File
|
|
self.notify.info("handleSuperBingoClose: Saving Hood Jackpots to DB")
|
|
self.notify.info("handleSuperBingoClose: hoodJackpots %s" %(self.__hoodJackpots))
|
|
for hood in list(self.__hoodJackpots.keys()):
|
|
if self.__hoodJackpots[hood][1]:
|
|
self.__hoodJackpots[hood][0] += BG.ROLLOVER_AMOUNT
|
|
# clamp it if it exceeds jackpot total
|
|
if self.__hoodJackpots[hood][0] > BG.MAX_SUPER_JACKPOT:
|
|
self.__hoodJackpots[hood][0] = BG.MAX_SUPER_JACKPOT
|
|
else:
|
|
self.__hoodJackpots[hood][1] = BG.MIN_SUPER_JACKPOT
|
|
|
|
taskMgr.remove(task)
|
|
self.save()
|
|
if self.finalGame:
|
|
self.shutdown()
|
|
return
|
|
|
|
self.waitForIntermission()
|
|
|
|
############################################################
|
|
# Method: __handleIntermission
|
|
# Purpose: This wrapper method tells the intermission to
|
|
# start.
|
|
# Input: task - a task that is spawned by a doMethodLater
|
|
# Output: None
|
|
############################################################
|
|
def __handleIntermission(self, task):
|
|
self.__startIntermission()
|
|
|
|
############################################################
|
|
# Method: getIntermissionTime
|
|
# Purpose: This method returns the time of when an
|
|
# intermission began. It is meant to provide a
|
|
# fairly accurate time countdown for the clients.
|
|
# Input: None
|
|
# Output: returns the timestamp of intermission start
|
|
############################################################
|
|
def getIntermissionTime(self):
|
|
return self.timeStamp
|
|
|
|
############################################################
|
|
# Method: __startIntermission
|
|
# Purpose: This method is responsible for starting the
|
|
# hourly intermission for bingo night.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def __startIntermission(self):
|
|
for bingoMgr in list(self.doId2do.values()):
|
|
bingoMgr.setFinalGame(BG.INTERMISSION)
|
|
|
|
if not self.finalGame:
|
|
currentTime = time.localtime()
|
|
currentMin = currentTime[4]
|
|
currentSec = currentTime[5]
|
|
|
|
# Calculate time until the next hour
|
|
waitTime = (60-currentMin)*60 - currentSec
|
|
sec = (currentMin - BG.HOUR_BREAK_MIN)*60 + currentSec
|
|
self.timeStamp = globalClockDelta.getRealNetworkTime() - sec
|
|
self.notify.info('__startIntermission: Timestamp %s'%(self.timeStamp))
|
|
else:
|
|
# In case someone should decide that bingo night does not end on the hour, ie 30 past,
|
|
# then this will allow a five minute intermission to sync up the PBMgrAIs for the
|
|
# final game.
|
|
waitTime = BG.HOUR_BREAK_SESSION
|
|
self.timeStamp = globalClockDelta.getRealNetworkTime()
|
|
|
|
self.waitTaskName = 'waitForEndOfIntermission'
|
|
self.notify.info('__startIntermission: Waiting %s seconds until Bingo Night resumes.' %(waitTime))
|
|
taskMgr.doMethodLater(waitTime, self.__resumeBingoNight, self.waitTaskName)
|
|
return Task.done
|
|
|
|
############################################################
|
|
# Method: __waitForIntermission
|
|
# Purpose: This method is responsible for calculating the
|
|
# wait time for the hourly intermission for bingo
|
|
# night.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def waitForIntermission(self):
|
|
currentTime = time.localtime()
|
|
currentMin = currentTime[4]
|
|
currentSec = currentTime[5]
|
|
|
|
# Calculate Amount of time needed for one normal game of Bingo from the
|
|
# Waitcountdown all the way to the gameover. (in secs)
|
|
if currentMin >= BG.HOUR_BREAK_MIN:
|
|
# If the AI starts during bingo night and after the intermission start(a crash or scheduled downtime),
|
|
# then immediately start the intermission to sync all the clients up for the next hour.
|
|
self.__startIntermission()
|
|
else:
|
|
waitTime = ((BG.HOUR_BREAK_MIN - currentMin)*60) - currentSec
|
|
self.waitTaskName = 'waitForIntermission'
|
|
self.notify.info("Waiting %s seconds until Final Game of the Hour should be announced." % (waitTime))
|
|
taskMgr.doMethodLater(waitTime, self.__handleIntermission, self.waitTaskName)
|
|
|
|
############################################################
|
|
# Method: generateBingoManagers
|
|
# Purpose: This method creates a PondBingoManager for each
|
|
# pond that is found within the hoods. It searches
|
|
# through each hood for pond objects and generates
|
|
# the corresponding ManagerAI objects.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def generateBingoManagers(self):
|
|
# Create DPBMAI for all ponds in all hoods.
|
|
for hood in self.air.hoods:
|
|
self.createPondBingoMgrAI(hood)
|
|
|
|
# Create DPBMAI for every pond in every active estate.
|
|
for estateAI in list(self.air.estateMgr.estate.values()):
|
|
self.createPondBingoMgrAI(estateAI)
|
|
|
|
############################################################
|
|
# Method: addDistObj
|
|
# Purpose: This method adds the newly created Distributed
|
|
# object to the BingoManagerAI doId2do list for
|
|
# easy reference.
|
|
# Input: distObj
|
|
# Output: None
|
|
############################################################
|
|
def addDistObj(self, distObj):
|
|
self.notify.debug("addDistObj: Adding %s : %s" % (distObj.getDoId(), distObj.zoneId))
|
|
self.doId2do[distObj.getDoId()] = distObj
|
|
self.zoneId2do[distObj.zoneId] = distObj
|
|
|
|
def __hoodToUse(self, zoneId):
|
|
hood = ZoneUtil.getCanonicalHoodId(zoneId)
|
|
if hood >= TTG.DynamicZonesBegin:
|
|
hood = TTG.MyEstate
|
|
|
|
return hood
|
|
|
|
############################################################
|
|
# Method: createPondBingoMgrAI
|
|
# Purpose: This method generates PBMgrAI instances for
|
|
# each pond found in the specified hood. A hood
|
|
# may be an estate or an actual hood.
|
|
# Input: hood - HoodDataAI or EstateAI object.
|
|
# dynamic - Will be 1 only if an Estate was generated
|
|
# after Bingo Night has started.
|
|
# Output: None
|
|
############################################################
|
|
def createPondBingoMgrAI(self, hood, dynamic=0):
|
|
if hood.fishingPonds == None:
|
|
self.notify.warning("createPondBingoMgrAI: hood doesn't have any ponds... were they deleted? %s" % hood)
|
|
return
|
|
|
|
for pond in hood.fishingPonds:
|
|
# First, optain hood id based on zone id that the pond is located in.
|
|
hoodId = self.__hoodToUse(pond.zoneId)
|
|
if hoodId not in self.hood2doIdList:
|
|
# for now don't start it for minigolf zone and outdoor zone
|
|
continue
|
|
|
|
bingoMgr = DistributedPondBingoManagerAI.DistributedPondBingoManagerAI(self.air, pond)
|
|
bingoMgr.generateWithRequired(pond.zoneId)
|
|
|
|
self.addDistObj(bingoMgr)
|
|
if hasattr(hood, "addDistObj"):
|
|
hood.addDistObj(bingoMgr)
|
|
pond.setPondBingoManager(bingoMgr)
|
|
|
|
# Add the PBMgrAI reference to the hood2doIdList.
|
|
self.hood2doIdList[hoodId].append(bingoMgr.getDoId())
|
|
|
|
# Dynamic if this method was called when an estate was generated after
|
|
# Bingo Night has started.
|
|
if dynamic:
|
|
self.startDynPondBingoMgrAI(bingoMgr)
|
|
|
|
############################################################
|
|
# Method: startDynPondBingoMgrAI
|
|
# Purpose: This method determines what state a Dynamic
|
|
# Estate PBMgrAI should start in, and then it tells
|
|
# the PBMgrAI to start.
|
|
# Input: bingoMgr - PondBongoMgrAI Instance
|
|
# Output: None
|
|
############################################################
|
|
def startDynPondBingoMgrAI(self, bingoMgr):
|
|
currentMin = time.localtime()[4]
|
|
|
|
# If the dynamic estate is generated before the intermission starts
|
|
# and it is not the final game of the night, then the PBMgrAI should start
|
|
# in the WaitCountdown state. Otherwise, it should start in the intermission
|
|
# state so that it can sync up with all of the other Estate PBMgrAIs for the
|
|
# super bingo game.
|
|
initState = (((currentMin < BG.HOUR_BREAK_MIN) and (not self.finalGame)) and ['WaitCountdown'] or ['Intermission'])[0]
|
|
bingoMgr.startup(initState)
|
|
|
|
############################################################
|
|
# Method: removePondBingoMgrAI
|
|
# Purpose: This method generates PBMgrAI instances for
|
|
# each pond found in the specified hood. A hood
|
|
# may be an estate or an actual hood.
|
|
# Input: doId - the doId of the PBMgrAI that should be
|
|
# removed from the dictionaries.
|
|
# Output: None
|
|
############################################################
|
|
def removePondBingoMgrAI(self, doId):
|
|
if doId in self.doId2do:
|
|
zoneId = self.doId2do[doId].zoneId
|
|
self.notify.info('removePondBingoMgrAI: Removing PondBingoMgrAI %s' %(zoneId))
|
|
hood = self.__hoodToUse(zoneId)
|
|
self.hood2doIdList[hood].remove(doId)
|
|
del self.zoneId2do[zoneId]
|
|
del self.doId2do[doId]
|
|
else:
|
|
self.notify.debug('removeBingoManager: Attempt to remove invalid PondBingoManager %s' % (doId))
|
|
|
|
############################################################
|
|
# Method: SetFishForPlayer
|
|
# Purpose: This method adds the newly created Distributed
|
|
# object to the BingoManagerAI doId2do list for
|
|
# easy reference.
|
|
# Input: distObj
|
|
# Output: None
|
|
############################################################
|
|
def setAvCatchForPondMgr(self, avId, zoneId, catch):
|
|
self.notify.info('setAvCatchForPondMgr: zoneId %s' %(zoneId))
|
|
if zoneId in self.zoneId2do:
|
|
self.zoneId2do[zoneId].setAvCatch(avId, catch)
|
|
else:
|
|
self.notify.info('setAvCatchForPondMgr Failed: zoneId %s' %(zoneId))
|
|
|
|
############################################################
|
|
# Method: getFileName
|
|
# Purpose: This method constructs the jackpot filename for
|
|
# a particular shard.
|
|
# Input: None
|
|
# Output: returns jackpot filename
|
|
############################################################
|
|
def getFileName(self):
|
|
"""Figure out the path to the saved state"""
|
|
f = "%s%s.jackpot" % (self.serverDataFolder, self.shard)
|
|
return f
|
|
|
|
############################################################
|
|
# Method: saveTo
|
|
# Purpose: This method saves the current jackpot ammounts
|
|
# to the specified file.
|
|
# Input: file - file to save jackpot amounts
|
|
# Output: None
|
|
############################################################
|
|
def saveTo(self, file):
|
|
pickle.dump(self.__hoodJackpots, file)
|
|
|
|
############################################################
|
|
# Method: save
|
|
# Purpose: This method determines where to save the jackpot
|
|
# amounts.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def save(self):
|
|
"""Save data to default location"""
|
|
try:
|
|
fileName = self.getFileName()
|
|
backup = fileName+ '.jbu'
|
|
if os.path.exists(fileName):
|
|
os.rename(fileName, backup)
|
|
|
|
file = open(fileName, 'wb')
|
|
file.seek(0)
|
|
self.saveTo(file)
|
|
file.close()
|
|
if os.path.exists(backup):
|
|
os.remove(backup)
|
|
except EnvironmentError:
|
|
self.notify.warning(str(sys.exc_info()[1]))
|
|
|
|
############################################################
|
|
# Method: loadFrom
|
|
# Purpose: This method loads the jackpot amounts from the
|
|
# specified file.
|
|
# Input: File - file to load amount from
|
|
# Output: returns a dictionary of the jackpots for this shard
|
|
############################################################
|
|
def loadFrom(self, file):
|
|
# Default Jackpot Amount
|
|
jackpots = self.DefaultReward
|
|
try:
|
|
jackpots = pickle.load(file)
|
|
except EOFError:
|
|
pass
|
|
return jackpots
|
|
|
|
############################################################
|
|
# Method: load
|
|
# Purpose: This method determines where to load the jackpot
|
|
# amounts.
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def load(self):
|
|
"""Load Jackpot data from default location"""
|
|
fileName = self.getFileName()
|
|
try:
|
|
file = open(fileName+'.jbu', 'rb')
|
|
if os.path.exists(fileName):
|
|
os.remove(fileName)
|
|
except IOError:
|
|
try:
|
|
file = open(fileName)
|
|
except IOError:
|
|
# Default Jackpot Amount
|
|
return self.DefaultReward
|
|
|
|
file.seek(0)
|
|
jackpots = self.loadFrom(file)
|
|
file.close()
|
|
return jackpots
|
|
|
|
############################################################
|
|
# Method: getSuperJackpot
|
|
# Purpose: This method returns the super jackpot amount for
|
|
# the specified zone. It calculates which hood
|
|
# the zone is in and returns the shared jackpot
|
|
# amount for that hood.
|
|
# Input: zoneId - retrieve jackpot for this zone's hood
|
|
# Output: returns jackpot for hood that zoneid is found in
|
|
############################################################
|
|
def getSuperJackpot(self, zoneId):
|
|
hood = self.__hoodToUse(zoneId)
|
|
self.notify.info('getSuperJackpot: hoodJackpots %s \t hood %s' % (self.__hoodJackpots, hood))
|
|
return self.__hoodJackpots.get(hood, [BG.MIN_SUPER_JACKPOT])[0]
|
|
|
|
############################################################
|
|
# Method: __startCloseEvent
|
|
# Purpose: This method starts to close Bingo Night down. One
|
|
# more super card game will be played at the end
|
|
# of the hour(unless the times are changed).
|
|
# Input: None
|
|
# Output: None
|
|
############################################################
|
|
def __startCloseEvent(self):
|
|
self.finalGame = BG.CLOSE_EVENT
|
|
|
|
if self.waitTaskName == 'waitForIntermission':
|
|
taskMgr.remove(self.waitTaskName)
|
|
self.__startIntermission()
|
|
|
|
############################################################
|
|
# Method: handleSuperBingoWin
|
|
# Purpose: This method handles a victory when a super
|
|
# bingo game has been one. It updates the jackpot
|
|
# amount and tells each of the other ponds in that
|
|
# hood that they did not win.
|
|
# Input: zoneId - pond who won the bingo game.
|
|
# Output: None
|
|
############################################################
|
|
def handleSuperBingoWin(self, zoneId):
|
|
# Reset the Jackpot and unmark the dirty bit.
|
|
|
|
hood = self.__hoodToUse(zoneId)
|
|
self.__hoodJackpots[hood][0] = self.DefaultReward[hood][0]
|
|
self.__hoodJackpots[hood][1] = 0
|
|
|
|
# tell everyone who won
|
|
#simbase.air.newsManager.bingoWin(zoneId)
|
|
|
|
# Tell the other ponds that they did not win and should handle the loss
|
|
for doId in self.hood2doIdList[hood]:
|
|
distObj = self.doId2do[doId]
|
|
if distObj.zoneId != zoneId:
|
|
self.notify.info("handleSuperBingoWin: Did not win in zone %s" %(distObj.zoneId))
|
|
distObj.handleSuperBingoLoss()
|
|
|
|
|