451 lines
22 KiB
Python
451 lines
22 KiB
Python
import random, team, player, os, math
|
|
from team import Team
|
|
from player import Player, Skater, Goalie
|
|
from skillContests import SkillContestParams, Situations, AtkAction, DefAction
|
|
from attributes import normalDis
|
|
from hocUtils import RinkGraph
|
|
from enum import Enum
|
|
import networkx as nx
|
|
|
|
RINKDIRPATH = os.path.join("Rinks","Graphs")
|
|
DEFAULTRINKFILENAME = "defaultedges.nx"
|
|
|
|
class Game(object):
|
|
"""A game of hockey!"""
|
|
|
|
def __init__(self, awayTeam:Team, homeTeam:Team, threes:bool=False):
|
|
if awayTeam is not None and homeTeam is not None:
|
|
#initial setup
|
|
self.away = awayTeam
|
|
self.home = homeTeam
|
|
|
|
self.awayScore = 0
|
|
self.homeScore = 0
|
|
|
|
self.awayZones = RinkGraph(edgeFilename=DEFAULTRINKFILENAME)
|
|
self.homeZones = RinkGraph(edgeFilename=DEFAULTRINKFILENAME)
|
|
self.currentZone = 23 #home defensive center
|
|
self.faceoffSpot = FaceoffDot.Center
|
|
self.playStopped = True
|
|
self.gameOver = False
|
|
|
|
#determine skater/goalie linups
|
|
self.lineSize = 5
|
|
if len(awayTeam.roster) != 10 or len(homeTeam.roster) != 10 or threes:
|
|
self.lineSize = 3
|
|
|
|
self.goalieHome = self.home.chooseGoalie()
|
|
self.goalieAway = self.away.chooseGoalie()
|
|
self.skatersHome = self.home.roster[:5] #LW LD C RD RW, EA; use the SkaterPosition enum for indexing. Threes uses left winger, left defenseman, center.
|
|
self.skatersAway = self.away.roster[:5]
|
|
|
|
self.positionInPossession = SkaterPosition.C #use SkaterPosition enum
|
|
self.teamInPossession = self.home
|
|
self.loosePuck = True
|
|
|
|
self.penaltyBoxAway = []
|
|
self.penaltyBoxHome = []
|
|
self.pulledGoalieAway = False
|
|
self.pulledGoalieHome = False
|
|
|
|
self.period = 1
|
|
self.startClock = 60*20 #Set clock to zhis value after each period
|
|
self.clock = self.startClock*1 #Time remaining in period, given in seconds
|
|
self.powerPlayEndTimes = []
|
|
|
|
self.eventLog = []
|
|
self.eventLogVerbose = []
|
|
|
|
#event affecting next event tracking
|
|
self.noDefender = False #breakaways
|
|
self.ineligibleDefenders = [] #prevent defender continuing to cause issues after being neutralized
|
|
self.space = False #give space-making passes and skates an advantage
|
|
self.loosePuckDefAdv = False #if attacker gets removed from play, defending team more likely to retrieve loose puck
|
|
|
|
def attackingSkater(self):
|
|
return self.skatersInPossession()[self.positionInPossession.value]
|
|
|
|
def defendingSkater(self):
|
|
"""Randomly selects a defensive skater to act as defender based on node"""
|
|
left = int(self.currentZone) >= 30 #defensive LW or LD
|
|
if self.positionInPossession not in [SkaterPosition.LD, SkaterPosition.RD]:
|
|
#FW in possession
|
|
if left:
|
|
counts = [2,5,3,4,1]
|
|
else:
|
|
counts = [1,4,3,5,2]
|
|
else:
|
|
#D in possession
|
|
if left:
|
|
counts = [5,3,4,1,2]
|
|
else:
|
|
counts = [2,1,4,3,5]
|
|
return self.skatersDefending()[random.sample(self.allPositions(), 1, counts=counts)[0].value]
|
|
|
|
def defendingTeam(self):
|
|
if self.teamInPossession == self.home:
|
|
return self.away
|
|
else:
|
|
return self.home
|
|
|
|
def attackingTeam(self):
|
|
"""Alias for teamInPossession, to match defendingTeam()"""
|
|
return self.teamInPossession
|
|
|
|
def homeAttacking(self):
|
|
return self.teamInPossession == self.home
|
|
|
|
def skatersInPossession(self):
|
|
return self.skatersHome if self.homeAttacking() else self.skatersAway
|
|
|
|
def skatersDefending(self):
|
|
return self.skatersHome if not self.homeAttacking() else self.skatersAway
|
|
|
|
def defendingGoalie(self):
|
|
return self.goalieAway if self.homeAttacking() else self.goalieHome
|
|
|
|
def activeGraph(self):
|
|
return self.homeZones if self.homeAttacking() else self.awayZones
|
|
|
|
def clockToMinutesSeconds(self):
|
|
"""Returns a string MM:SS elapsed in period."""
|
|
elapsedSeconds = 60*20 - self.clock if self.clock >= 0 else 60*20
|
|
minutes = str(int(math.modf(elapsedSeconds/60)[1]))
|
|
seconds = str(elapsedSeconds % 60)
|
|
if len(seconds) == 1:
|
|
seconds = f"0{seconds}"
|
|
return f"{minutes}:{seconds}"
|
|
|
|
def allPositions(self):
|
|
"""Get a list of all SkaterPosition enums."""
|
|
return [
|
|
SkaterPosition.LW, SkaterPosition.LD, SkaterPosition.C, SkaterPosition.RD, SkaterPosition.RW
|
|
]
|
|
|
|
|
|
def currentSituation(self):
|
|
"""Returns a Situations enum based on current skater counts."""
|
|
skatersH = self.lineSize + self.pulledGoalieHome - len(self.penaltyBoxHome)
|
|
skatersA = self.lineSize + self.pulledGoalieAway - len(self.penaltyBoxAway)
|
|
if self.teamInPossession == self.home:
|
|
return Situations(skatersH - skatersA)
|
|
else:
|
|
return Situations(skatersA - skatersH)
|
|
|
|
def skillContest(self, atkPlayer:Player, defPlayer:Player, params:SkillContestParams):
|
|
"""Contests the two players with the given stats and stat weights. Returns True on offensive success."""
|
|
if params.override is not None:
|
|
return params.override
|
|
else:
|
|
atkValue = 0
|
|
defValue = 0
|
|
for attr, weight in params.atkStats:
|
|
atkValue += atkPlayer.getAttribute(attr).value * weight/100
|
|
for attr, weight in params.defStats:
|
|
defValue += defPlayer.getAttribute(attr).value * weight/100
|
|
|
|
atkRoll = normalDis(atkValue, atkValue/2, 0)
|
|
defRoll = normalDis(defValue, defValue/2, 0)
|
|
return atkRoll-defRoll > 0
|
|
|
|
def faceoffContest(self, awayPlayer:Skater, homePlayer:Skater):
|
|
"""Hold a faceoff! True indicates home win, False is away win."""
|
|
faceoffSkills = [('Dex',50),('Sti',50),('Pas', 10)]
|
|
params = SkillContestParams(faceoffSkills, faceoffSkills)
|
|
return self.skillContest(homePlayer, awayPlayer, params) if random.random() > 0.4 else random.sample([True, False], 1)[0]
|
|
|
|
def zoneAfterFaceoff(self):
|
|
if self.homeAttacking():
|
|
lookupList = [35, 15, 34, 14, 23, 32, 12, 31, 11]
|
|
else:
|
|
lookupList = [11, 31, 12, 32, 23, 14, 34, 15, 35]
|
|
return str(lookupList[self.faceoffSpot.value])
|
|
|
|
def event(self):
|
|
"""Meat and potatoes. Everyzhing zat happens is a direct result of zis being called."""
|
|
if self.clock < 0: #period/game over
|
|
if self.period >= 3 and self.homeScore != self.awayScore: #game over
|
|
self.gameOver = True
|
|
self.addEventLog(f"Final score: {self.away.name} {self.awayScore} - {self.homeScore} {self.home.name}")
|
|
else: #increment period, reset values
|
|
self.period += 1
|
|
self.clock = self.startClock*1
|
|
self.playStopped = True
|
|
self.faceoffSpot = FaceoffDot.Center
|
|
self.addEventLog(f"Period {self.period} begins!")
|
|
|
|
elif self.playStopped: #need a faceoff
|
|
self.teamInPossession = self.home if self.faceoffContest(self.skatersAway[SkaterPosition.C.value], self.skatersHome[SkaterPosition.C.value]) else self.away
|
|
self.positionInPossession = SkaterPosition(random.sample([0, 1, 1, 3, 3, 4],1)[0]) #wingers are less likely to recieve the faceoff than defensemen
|
|
self.playStopped = False
|
|
winningPlayer = self.skatersInPossession()[SkaterPosition.C.value]
|
|
receivingPlayer = self.skatersInPossession()[self.positionInPossession.value]
|
|
eventString = f"{self.teamInPossession.shortname} {winningPlayer} wins faceoff to {receivingPlayer}"
|
|
self.addEventLog(eventString)
|
|
self.clock -= random.randint(2,5)
|
|
self.currentZone = self.zoneAfterFaceoff()
|
|
|
|
elif self.loosePuck: #who gets it
|
|
#first pick skaters chasing puck
|
|
defenders = int(self.currentZone) % 10 <= 3
|
|
if defenders: #offensive team needs defensemen
|
|
validPosO = [SkaterPosition.LD, SkaterPosition.RD]
|
|
validPosD = [SkaterPosition.RW, SkaterPosition.LW]
|
|
else:
|
|
validPosO = [SkaterPosition.RW, SkaterPosition.LW]
|
|
validPosD = [SkaterPosition.LD, SkaterPosition.RD]
|
|
validPosO.append(SkaterPosition.C)
|
|
validPosD.append(SkaterPosition.C) #center can always chase :>
|
|
atkPos = random.sample(validPosO,1)[0]
|
|
defPos = random.sample(validPosD,1)[0]
|
|
atkChaser = self.skatersInPossession()[atkPos.value]
|
|
defChaser = self.skatersDefending()[defPos.value]
|
|
puckChaseSkills = [('Spe',50), ('Int',25), ('Agi',25)]
|
|
params = SkillContestParams(puckChaseSkills, puckChaseSkills)
|
|
result = self.skillContest(atkChaser, defChaser, params)
|
|
if result: #offensive skater gets it
|
|
self.positionInPossession = atkPos
|
|
else:
|
|
self.positionInPossession = defPos
|
|
self.changePossession()
|
|
self.addEventLog(f"{self.attackingSkater()} chases za puck down for {self.attackingTeam().shortname}")
|
|
self.loosePuck = False
|
|
self.clock -= random.randint(3,8)
|
|
|
|
else: #run za state machine
|
|
validActions = self.activeGraph().getAllReachableFrom(self.currentZone)
|
|
attacker = self.attackingSkater()
|
|
defender = self.defendingSkater()
|
|
while defender in self.ineligibleDefenders:
|
|
defender = self.defendingSkater() #reroll until eligible defender found
|
|
self.ineligibleDefenders = [] #clear ineligible defs
|
|
|
|
atkAction, nodeTarget = attacker.chooseAtkAction(validActions, self.currentZone, self.activeGraph(), defender)
|
|
defAction = defender.chooseDefAction(self.currentZone, self.activeGraph())
|
|
|
|
scParams = SkillContestParams().actionCheck(atkAction, defAction, self.currentSituation())
|
|
result = self.skillContest(attacker, defender, scParams)
|
|
if result: #attacker succeeded
|
|
if atkAction in [AtkAction.ShotS, AtkAction.ShotW]: #shot
|
|
self.addEventLog(f"{attacker} takes a shot!")
|
|
self.goalieCheck(atkAction, attacker) #shot goes zhrough
|
|
else:
|
|
self.currentZone = int(nodeTarget)
|
|
if atkAction in [AtkAction.PassB, AtkAction.PassF, AtkAction.PassS]: #pass
|
|
self.space = atkAction == AtkAction.PassB #backpasses create space for next action
|
|
#successful pass, determine new possession
|
|
allPos = self.allPositions()
|
|
allPos.remove(self.positionInPossession) #cant pass to yourself
|
|
if int(nodeTarget) % 10 >= 6: #D wouldnt be behind net, ever
|
|
if self.positionInPossession != SkaterPosition.RD:
|
|
allPos.remove(SkaterPosition.RD)
|
|
if self.positionInPossession != SkaterPosition.LD:
|
|
allPos.remove(SkaterPosition.LD)
|
|
#emphasize pass side attacker
|
|
if int(nodeTarget) < 30: #attacking left
|
|
if self.positionInPossession != SkaterPosition.LW:
|
|
allPos.append(SkaterPosition.LW)
|
|
else:
|
|
allPos.append(SkaterPosition.C)
|
|
else:
|
|
if self.positionInPossession != SkaterPosition.RW:
|
|
allPos.append(SkaterPosition.RW)
|
|
else:
|
|
allPos.append(SkaterPosition.C)
|
|
self.positionInPossession = random.sample(allPos, 1)[0]
|
|
self.ineligibleDefenders.append(defender)
|
|
self.addEventLog(f"{attacker} passes to {self.attackingSkater()}.")
|
|
self.clock -= random.randint(1,3) #passes are quick
|
|
elif atkAction in [AtkAction.SkateA, AtkAction.SkateF, AtkAction.SkateT, AtkAction.SkateB]:
|
|
if atkAction == AtkAction.SkateB:
|
|
self.space = True
|
|
else:
|
|
self.space = False
|
|
self.ineligibleDefenders.append(defender) #got around 'em
|
|
self.addEventLog(f"{attacker} skates to node {nodeTarget}.", verbose=True)
|
|
self.clock -= random.randint(3,6) #skating is slow
|
|
else: #dumped puck
|
|
raise NotImplementedError
|
|
else: #defender won
|
|
if defAction in [DefAction.Force, DefAction.Steal, DefAction.Body]: #actions zat grant defender puck at start of action
|
|
self.changePossession()
|
|
self.positionInPossession = SkaterPosition(self.skatersInPossession().index(defender))
|
|
if defAction == DefAction.Body:
|
|
self.addEventLog(f"{defender} bodies {attacker} off za puck.")
|
|
else:
|
|
self.addEventLog(f"{defender} steals it from {attacker}.")
|
|
self.clock -= random.randint(4,6)
|
|
elif defAction in [DefAction.Pin, DefAction.Poke]: #actions zat cause loose puck at start of action
|
|
self.loosePuck = True
|
|
self.loosePuckDefAdv = defAction == DefAction.Poke
|
|
self.currentZone = int(random.sample(self.activeGraph().getAdjacentNodes(self.currentZone), 1)[0])
|
|
self.addEventLog(f"{defender} forces za puck loose!")
|
|
self.clock -= random.randint(2,4)
|
|
elif defAction == DefAction.BlockSlot: #grants defender puck at end of action
|
|
self.currentZone = nodeTarget
|
|
self.changePossession()
|
|
self.positionInPossession = SkaterPosition(self.skatersInPossession().index(defender))
|
|
self.addEventLog(f"{defender} blocks a shot and picks up za puck!")
|
|
self.clock -= random.randint(1,3)
|
|
elif defAction == DefAction.BlockLn: #pass fuckery
|
|
self.passCheck(nodeTarget, defender, atkAction)
|
|
self.clock -= random.randint(3,6)
|
|
|
|
def passCheck(self, target, blockingDefender, passType):
|
|
if passType == AtkAction.PassS or random.random()*100 < normalDis(blockingDefender.getAttribute("Ref").value,30,20,80): #stretch pass always intercepted, chance for interception based on defender's reflexes
|
|
self.currentZone = target
|
|
self.changePossession()
|
|
self.positionInPossession = SkaterPosition(self.skatersInPossession().index(blockingDefender))
|
|
self.addEventLog(f"{blockingDefender} intercepts a pass!")
|
|
else: #loose puck!
|
|
if random.random() > 0.5:
|
|
self.currentZone = target
|
|
self.loosePuck = True
|
|
self.loosePuckDefAdv = True
|
|
self.addEventLog(f"Za pass is knocked loose!")
|
|
|
|
|
|
|
|
def goalieCheck(self, shotType, shooter):
|
|
atkMult = self.activeGraph().shotDanger(self.currentZone)/100 #worse shots furzher out
|
|
if shotType == AtkAction.ShotS: #slapshot
|
|
shooterSkills = [('Sho', 80*atkMult), ('Str', 30*atkMult)]
|
|
goalieSkills = [('Ref', 90)]
|
|
else: #wristshot
|
|
shooterSkills = [('Sho', 50*atkMult), ('Dex', 30*atkMult)]
|
|
goalieSkills = [('Ref', 80),('Agi', 20)]
|
|
params = SkillContestParams(shooterSkills, goalieSkills)
|
|
result = self.skillContest(shooter,self.defendingGoalie(),params)
|
|
if result: #GOAL
|
|
if self.homeAttacking():
|
|
self.homeScore += 1
|
|
else:
|
|
self.awayScore += 1
|
|
self.playStopped = True
|
|
self.addEventLog(f"{shooter} scores wizh a {shotType.name}! New score: {self.away.shortname} {self.awayScore} - {self.homeScore} {self.home.shortname}")
|
|
self.faceoffSpot = FaceoffDot.Center
|
|
elif random.randint(0,100) < normalDis(self.defendingGoalie().getAttribute('Dex').value,75,0,100): #caught puck
|
|
self.saveMadeStop(shooter, shotType)
|
|
else: #blocked shot
|
|
self.loosePuck = True
|
|
self.loosePuckDefAdv = True
|
|
self.currentZone = random.sample(self.activeGraph().getAdjacentNodes(27),1)[0]
|
|
self.addEventLog(f"{shotType.name} shot knocked aside by {self.defendingGoalie()}.")
|
|
self.clock -= random.randint(2,6)
|
|
|
|
def changePossession(self):
|
|
self.teamInPossession = self.away if self.homeAttacking() else self.home
|
|
#gotta flip node
|
|
self.currentZone = 47 - int(self.currentZone)
|
|
|
|
def saveMadeStop(self, shootingPlayer, shotType):
|
|
"""Stops play due to a save made by a goalie, and sets the faceoff dot to be used."""
|
|
self.playStopped = True
|
|
eventText = f"{self.defendingGoalie()} saves shot from {shootingPlayer}, stops play."
|
|
self.eventLog.append(eventText)
|
|
self.eventLogVerbose.append(eventText)
|
|
options = [FaceoffDot.AwayZoneLeft, FaceoffDot.AwayZoneRight] if self.homeAttacking() else [FaceoffDot.HomeZoneLeft, FaceoffDot.HomeZoneRight]
|
|
self.faceoffSpot = random.sample(options,1)[0]
|
|
|
|
def stopPlay(self):
|
|
"""Stops play due to a knocked down puck, puck in the netting or benches, or otherwise generic call that needs the closest dot."""
|
|
self.playStopped = True
|
|
self.eventLogVerbose.append("Play whistled dead.")
|
|
if self.homeAttacking():
|
|
defenseDots = [FaceoffDot.HomeZoneRight, FaceoffDot.HomeZoneLeft]
|
|
neutralDefDots = [FaceoffDot.HomeNeutralRight, FaceoffDot.HomeNeutralLeft, FaceoffDot.Center]
|
|
neutralOffDots = [FaceoffDot.AwayNeutralRight, FaceoffDot.AwayNeutralLeft, FaceoffDot.Center]
|
|
offenseDots = [FaceoffDot.AwayZoneRight, FaceoffDot.AwayZoneLeft]
|
|
else:
|
|
offenseDots = [FaceoffDot.HomeZoneRight, FaceoffDot.HomeZoneLeft]
|
|
neutralOffDots = [FaceoffDot.HomeNeutralRight, FaceoffDot.HomeNeutralLeft, FaceoffDot.Center]
|
|
neutralDefDots = [FaceoffDot.AwayNeutralRight, FaceoffDot.AwayNeutralLeft, FaceoffDot.Center]
|
|
defenseDots = [FaceoffDot.AwayZoneRight, FaceoffDot.AwayZoneLeft]
|
|
if int(self.currentZone[1]) <= 2: #defensive end
|
|
dots = defenseDots
|
|
elif int(self.currentZone[1]) >= 5: #offensive end
|
|
dots = offenseDots
|
|
elif int(self.currentZone[1]) == 3:
|
|
dots = neutralDefDots
|
|
else:
|
|
dots = neutralOffDots
|
|
self.faceoffSpot = random.sample(dots, 1)[0]
|
|
|
|
def stopPlayOffsides(self):
|
|
"""Stops play due to an offsides call."""
|
|
self.playStopped = True
|
|
eventText = f"{self.teamInPossession.shortname} goes offsides."
|
|
self.eventLog.append(eventText)
|
|
self.eventLogVerbose(eventText)
|
|
if self.homeAttacking():
|
|
dots = [FaceoffDot.AwayNeutralLeft, FaceoffDot.AwayNeutralRight]
|
|
else:
|
|
dots = [FaceoffDot.HomeNeutralLeft, FaceoffDot.HomeNeutralRight]
|
|
self.faceoffSpot = random.sample(dots, 1)[0]
|
|
|
|
def stopPlayIcing(self):
|
|
"""Stops play due to an icing call."""
|
|
self.playStopped = True
|
|
eventText = f"{self.teamInPossession.shortname} ices the puck."
|
|
self.eventLog.append(eventText)
|
|
self.eventLogVerbose(eventText)
|
|
if self.homeAttacking():
|
|
dots = [FaceoffDot.HomeZoneLeft, FaceoffDot.HomeZoneRight]
|
|
else:
|
|
dots = [FaceoffDot.AwayZoneLeft, FaceoffDot.AwayZoneRight]
|
|
self.faceoffSpot = random.sample(dots, 1)[0]
|
|
|
|
def stopPlayPenalty(self, offendingPlayer:Player, penaltyText:str):
|
|
"""Stops play due to an icing call."""
|
|
self.playStopped = True
|
|
ppTeam = self.home if self.away.isPlayerOnTeam(offendingPlayer) else self.away
|
|
eventText = f"{str(self.offendingPlayer)} is called for {penaltyText}. {ppTeam.shortname} is on the powerplay."
|
|
self.powerPlaySetup()
|
|
self.eventLog.append(eventText)
|
|
self.eventLogVerbose(eventText)
|
|
if self.homeAttacking():
|
|
dots = [FaceoffDot.HomeZoneLeft, FaceoffDot.HomeZoneRight]
|
|
else:
|
|
dots = [FaceoffDot.AwayZoneLeft, FaceoffDot.AwayZoneRight]
|
|
self.faceoffSpot = random.sample(dots, 1)[0]
|
|
|
|
def powerPlaySetup(self):
|
|
raise NotImplementedError()
|
|
|
|
def addEventLog(self, eventString, verbose:bool=False):
|
|
self.eventLogVerbose.append(f"{self.clockToMinutesSeconds()} - {eventString}")
|
|
if not verbose:
|
|
self.eventLog.append(eventString)
|
|
|
|
def eventLogLength(self):
|
|
count = 0
|
|
for line in self.eventLog:
|
|
count += len(line)+1 #the extra 1 is for the newline at the end
|
|
return count
|
|
|
|
def eventLogOut(self):
|
|
outList = []
|
|
while len(self.eventLogVerbose) > 0:
|
|
outList.append(self.eventLogVerbose.pop(0))
|
|
return outList
|
|
|
|
class FaceoffDot(Enum):
|
|
"""All orientations are given from the perspective of the defending team."""
|
|
AwayZoneLeft = 0
|
|
AwayZoneRight = 1
|
|
AwayNeutralLeft = 2
|
|
AwayNeutralRight = 3
|
|
Center = 4
|
|
HomeNeutralRight = 5
|
|
HomeNeutralLeft = 6
|
|
HomeZoneRight = 7
|
|
HomeZoneLeft = 8
|
|
|
|
class SkaterPosition(Enum):
|
|
"""Allows easy indexing to the active skaters lists for each team."""
|
|
LW = 0
|
|
LD = 1
|
|
C = 2
|
|
RD = 3
|
|
RW = 4
|
|
EA = 5 |