oldschool-toontown/toontown/fishing/BingoManagerAI.py
2024-07-14 15:28:28 -05:00

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()