Code
Godot Game Engine
- Open-Source Game Engine
- GDScript, Python-like scripting language
- Nodes for various uses
- Flexible Scene-System, Nodetrees
- Visual Editor for Scenes
- Easy deployment to many platforms
- Comes with a multi-platform editor
- Many more features
Structure
The software consists of two scenes, five base classes and a global class with constants and static functions.The classes extending the scenes contain the main thread for the correspoding scene. They also function as an adapter between inputs and outputs.
The game-management class handles all the input from the Dashboard-scene and generates the output to the Dashboard-scene with data from the entity classes 'Country' and 'State'.
Both entity classes model the real-life, simulting and storing the data. Inside each state runs the main simulation function.
Main-Thread inside Dash Control Class
This is the function of the main thread. The _process function gets called every frame by Godot. As you can see the simulation runs in a different thread than the main thread to ensure that the interface doesn't freeze and stays usable.func _process(_delta): # called every frame, _delta is the elapsed time between two frames
if !paused:
if remainingDays > 0 and !running:
Constants.currentProgress = 0
statOutput[CONSTANTS.PROGRESSPANEL].visible = true
# For DEBUGGING
# self.running = true
# game_manager.simulate()
# self.remainingDays -= 1
# self.running = false
# For RUNNING
game_manager._simThread.wait_to_finish()
game_manager._simThread.start(self, "runSimulation", null)
updateProgress()
Inside Game-Management Class
This is the moment the game-management class passes on the order to simulate to the country and updates the current day. Additionally here it gets checked if the pandemic is over. If there are no new infections inside a certain time interval, the pandmic has ended.func simulate():
entities[CONSTANTS.DEU].simulateALL()
if self.days.size() > 10:
var checkNewInfections = []
for i in range(self.days.max() - int(CONSTANTS.MONTH * CONSTANTS.ENDEMICTIMEFACTOR), self.days.max()):
if i < 2:
continue
else:
checkNewInfections.append(entities[CONSTANTS.DEU].getDailyInfections(i, true))
if checkNewInfections.max() < 1:
setMode(CONSTANTS.ENDMODE)
if !ended:
endDay = currentDay
print("Day ", self.currentDay, ": PANDEMIC OVER")
updateDay()
Inside the Country Class
The country now produces the vaccine doses and distributes them. Also the commuters get distributed to the neighboring state here. To fasten this process, the distribution of commuters is run in threads. Just like this the simulation is also running in 16 Threads (like the 16 federal states of Germany).func simulateALL():
produceVax()
distributeVax()
distributeCommuters()
for state in states.values():
# state.simulate() # for easier debugging
state._thread.start(self, "simulateState", state.getName())
for state in states.values():
state._thread.wait_to_finish()
homeCommuters()
for state in states.values():
state.collectNumbers()
getNumbers()
recalculateStatePopulation()
Distributing Commuters
- Commuter-Rates based on data from the 'Agentur für Arbeit'
- Commuters get evenly distributed to neighboring states in round-robin style
- Randomly selected one by one from a category
- Positive tested, hospitalised and individuals inside the waiting interval don't commute
Inside the State Class
This is where the Gillespie-Algorithm is realised. Also here the individuals in the waiting interval for the second vaccination get handled. This has similarities to a conveyor belt, as each day the individuals get shafted one step ahead. Visible as well here is the change of infection-rates. With the infection-rates events can be altered in case of e.g. a lockdown.func simulate():
var startTime = OS.get_ticks_msec()
events = 0
infectRate = [getInfectRate(), getInfectRate()*infectTestFactor, getInfectRate()*infectFactorHosp, getInfectRate()*infectFactorV1, getInfectRate()*infectFactorV2] # untested, tested, hospitalised, 1x-vaxed, 2x-vaxed
var t = timeDifference
while t<1:
t = gillespieIteration(t)
events += 1
if(t>1):
timeDifference = fmod(t,1)
continue
waitDay += 1
waitDay = waitDay % Constants.VACDELAY
V1eligible[0] += V1[0][waitDay]
V1[0][waitDay] = 0
V1eligible[1] += V1[1][waitDay]
V1[1][waitDay] = 0
# and so on
Gillespie-Iteration
This is where one Gillespie-Event takes place. As you can see the time gets forwarded with -log(r1)/reactTotal, so that the number of events fit one day.func gillespieIteration(t):
var r1 = rnd.randf()
var reactionRates = updateReactionRates()
var reactTotal = CONSTANTS.sum(reactionRates)
if reactTotal == 0:
return 1
var waitTime = -log(r1)/reactTotal
t = t + waitTime
var r2 = rnd.randf()
var reactionRatesCumSum = CONSTANTS.cumulative_sum(reactionRates)
for i in range(reactionRatesCumSum.size()):
reactionRatesCumSum[i] = reactionRatesCumSum[i] / reactTotal
var rule
for i in range(reactionRatesCumSum.size()):
if(r2 <= reactionRatesCumSum[i]):
rule = i
break
updatePersonNumbers(rule)
return t
Reaction-Rates
There are in total 149 reaction rates. All of these rates are derived from the basic rates seen on the modeling page.func updateReactionRates():
var rates = []
# 0 1 2 3 Infection of untested, non-vaxed individuals
rates.append((infectRate[0]/population)*S[0]*I[0]) # by untested infected (non-vaxed)
rates.append((infectRate[1]/population)*S[0]*I[1]) # by tested infected (non-vaxed)
rates.append((infectRate[0]/population)*S[0]*I[2]) # by unknowingly infected (non-vaxed)
rates.append((infectRate[2]/population)*S[0]*I[3]) # by non-vaxed hospitalised
# many more rules, 149 in total
Updating the individual numbers
Some rates are mapped onto the same rules, because they trigger the same events. If an event happens to an individual inside the vaccination waiting period or a commuter, the rule gets carried out on a random member of the corresponding group.func updatePersonNumbers(rule):
match rule:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12: # Infection untested non-vaxed
S[0] -= 1
I[0] += 1
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25: # Infection tested non-vaxed
S[1] -= 1
I[2] += 1
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38: # Infection 1x-vaxed, that are in the waiting interval
var randomDay = rnd.randi() % Constants.VACDELAY # assign infection randomly to a waiting-block
while true:
if V1[0][randomDay] > 0:
break
else:
randomDay += 1
randomDay = randomDay % Constants.VACDELAY
V1[0][randomDay] -= 1
V1[1][randomDay] += 1
# 149 rates have to be matched to rules
Saving the data
GDScript allows arrays to have multiple data types. So at the beginning the name of the individual group gets saved and after that the data with the correspoding day as an index. This makes it easy to use the data to create output, because you always have the name available and can easily access the data via day.var sus0 = [CONSTANTS.NTESTED + CONSTANTS.SUSCEPTIBLE] # untested susceptible
var sus1 = [CONSTANTS.TESTED + CONSTANTS.SUSCEPTIBLE] # tested susceptible
var inf0 = [CONSTANTS.NTESTED + CONSTANTS.INFECTED] # untested infected
var inf1 = [CONSTANTS.TESTED + CONSTANTS.INFECTED] # tested infected
var inf2 = [CONSTANTS.UNAWARE + CONSTANTS.INFECTED] # unaware infected
var hosp = [CONSTANTS.HOSPITALISED] # non-vaxed hosptialised
var rec0 = [CONSTANTS.NTESTED + CONSTANTS.RECOVERED] # untested recovered
var rec1 = [CONSTANTS.TESTED + CONSTANTS.RECOVERED] # tested recovered
var rec2 = [CONSTANTS.UNAWARE + CONSTANTS.RECOVERED] # unaware recovered
var dead0 = [CONSTANTS.NTESTED + CONSTANTS.DEAD] # untested dead
var dead1 = [CONSTANTS.TESTED + CONSTANTS.DEAD] # tested dead
var dead2 = [CONSTANTS.UNAWARE + CONSTANTS.DEAD] # unaware dead
var vax1sus = [CONSTANTS.VAX1 + CONSTANTS.SUSCEPTIBLE] # 1x-vaxed susceptible
var vax1inf = [CONSTANTS.VAX1 + CONSTANTS.INFECTED] # 1x-vaxed infected
var vax1hosp = [CONSTANTS.VAX1 + CONSTANTS.HOSPITALISED] # 1x-vaxed hosptialised
var vax1rec = [CONSTANTS.VAX1 + CONSTANTS.RECOVERED] # 1x-vaxed recovered
var vax1dead = [CONSTANTS.VAX1 + CONSTANTS.DEAD] # 1x-vaxed dead
var vax2sus = [CONSTANTS.VAX2 + CONSTANTS.SUSCEPTIBLE] # 2x-vaxed susceptible
var vax2inf = [CONSTANTS.VAX2 + CONSTANTS.INFECTED] # 2x-vaxed infected
var vax2hosp = [CONSTANTS.VAX2 + CONSTANTS.HOSPITALISED] # 2x-vaxed hospitalised
var vax2rec = [CONSTANTS.VAX2 + CONSTANTS.RECOVERED] # 2x-vaxed recovered