#!/usr/bin/env python3
from typing import Generator, NamedTuple
HOURS_TO_PRODUCE_ASSEMBLER = 6 * 24 # 6 days * 24 hours/day = 144 hours
# equivalent constant, but this name is more legible in certain contexts
FIGHTERS_PRODUCED_IN_ONE_UNIT = HOURS_TO_PRODUCE_ASSEMBLER
class Strategy(NamedTuple):
available_hours: int
produced_fighters: int
class MaxStrategy(NamedTuple):
available_hours: int
produced_fighters: int
produced_assemblers: int
def assembler_schedule(available_hours) -> dict[int, Strategy]:
"""A schedule of quantities of assemblers that are possible within
`available_hours`.
Index the schedule by a target quantity of assemblers. This provides the
optimal strategy to produce that quantity. A strategy is represented by a
tuple of available hours and number of fighters produced incidentally.
The first assembler is given; there is no strategy for 0 assemblers.
If there is no strategy to produce a quantity given `available_hours`, the
schedule will not be indexable by that quantity.
"""
# initialize the 'do nothing' strategy
schedule = { 1: Strategy(available_hours, 0) }
while available_hours >= HOURS_TO_PRODUCE_ASSEMBLER:
# consume 6 days
available_hours -= HOURS_TO_PRODUCE_ASSEMBLER
# find the maximal number of assemblers at this hour
assemblers = max(schedule.keys())
# with N assemblers, for n in 1..N, n will produce assemblers
for i in range(1, assemblers + 1):
produced_assemblers = assemblers + i
produced_fighters = (assemblers - i) * HOURS_TO_PRODUCE_ASSEMBLER
schedule[produced_assemblers] = Strategy(available_hours, produced_fighters)
return schedule
def evaluate_fighters(strategy: Strategy, assemblers: int) -> int:
"""Helper function to calculate final production of fighters."""
return strategy.produced_fighters + (assemblers * strategy.available_hours)
def evaluate_schedule(schedule: dict[int, Strategy]) -> Generator[tuple[int, int, Strategy], None, None]:
"""For each strategy, determine how many fighters would be produced by
setting all assemblers to produce fighters for the availabe hours.
"""
for assemblers in schedule.keys():
fighters = evaluate_fighters(schedule[assemblers], assemblers)
yield (fighters, assemblers, schedule[assemblers])
def max_schedule(schedule: dict[int, Strategy]) -> MaxStrategy:
"""Find the maximal strategy within a schedule.
A tuple of hours remaining, fighters proeduced, and assemblers produced.
"""
# initialize with the 'do nothing' strategy
return max(tuple(evaluate_schedule(schedule)), key=lambda x: x[0])
def dump():
schedule = assembler_schedule(100 * 24)
print("FinalFighters,ProducedAssemblers,ProducedFighters,UnconsumedHours")
for fighters, assemblers, strategy in evaluate_schedule(schedule):
print(f"{fighters}\t{assemblers}\t{strategy.produced_fighters}\t{strategy.available_hours}")
def main():
schedule = assembler_schedule(100 * 24)
maximum = max_schedule(schedule)
print(f"Built {maximum[0]} fighters with {maximum[1]} assemblers")
print(f" * {maximum[2].produced_fighters} fighters were built concurrent to the assemblers")
print(f" * {maximum[0] - maximum[2].produced_fighters} fighters were built in the unconsumed {maximum[2].available_hours} hours")
if __name__ == "__main__":
main()