Last active
March 24, 2021 21:26
-
-
Save GiantDarth/f3cc801a3fa8bb51466f579065b777ae to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
import argparse | |
import os | |
import multiprocessing | |
import random | |
from itertools import product, product, permutations, combinations_with_replacement, repeat | |
from dataclasses import dataclass | |
from typing import List | |
@dataclass | |
class Ice: | |
adjacents: List[Ice] | |
# Any adjacents to a border or frozen has a chance to be frozen | |
border: bool = False | |
frozen: bool = False | |
# Used for any water blocked from the sun. These will never freeze | |
blocked: bool = False | |
def __str__(self): | |
if self.border: | |
return 'X' | |
elif self.blocked: | |
return 'W' | |
elif self.frozen: | |
return '\u2588' | |
else: | |
return '\u2591' | |
class IceFarm: | |
def __init__(self, size: int) -> None: | |
if size < 1: | |
raise ValueError("Size must be positive.") | |
self._size = size | |
# Ceiling divide | |
self._chunk_count = size // CHUNK_SIZE + (0 if size % CHUNK_SIZE == 0 else 1) | |
self._grid = [[Ice([]) for _ in range(size + 2)] for _ in range(size + 2)] | |
for i in range(size + 2): | |
for j in [0, size + 1]: | |
self._grid[i][j].border = True | |
for i in [0, size + 1]: | |
for j in range(size + 2): | |
self._grid[i][j].border = True | |
for i in range(1, size + 1): | |
self._grid[i][i].blocked = True | |
if size > 7: | |
for i in range(1, size + 1): | |
self._grid[i][self._size + 1 - i].blocked = True | |
for i in range(1, size + 1): | |
for j in range(1, size + 1): | |
# Ignores the diagonals since experimental evidence supports this | |
for x, y in [(i, j - 1), (i, j + 1), (i - 1, j), (i + 1, j)]: | |
if i != x or j != y: | |
self._grid[i][j].adjacents.append(self._grid[x][y]) | |
def print_adjacency(self): | |
for row in self._grid: | |
print(' '.join('X' if cell.border else str(len(cell.adjacents)) for cell in row)) | |
def __str__(self): | |
return '\n'.join(' '.join(str(cell) for cell in row) for row in self._grid) | |
def update(self): | |
# Treat the grid layout as Minecraft chunks, where each chunk has an independent weather check | |
for c_i, c_j in product(range(self._chunk_count), repeat=2): | |
if random.random() < WEATHER_UPDATE_CHANCE: | |
# Choose a random coordinate to weather update | |
i = random.randint(1, CHUNK_SIZE) + (c_i * CHUNK_SIZE) | |
j = random.randint(1, CHUNK_SIZE) + (c_j * CHUNK_SIZE) | |
if 0 < i < self._size + 2 and 0 < j < self._size + 2 and not self._grid[i][j].border and not self._grid[i][j].blocked: | |
if any(cell.frozen or cell.border for cell in self._grid[i][j].adjacents): | |
self._grid[i][j].frozen = True | |
def count(self): | |
""" | |
The number of ice blocks that currently exist. | |
""" | |
return sum(1 for i, j in product(range(self._size + 2), repeat=2) if not self._grid[i][j].border and self._grid[i][j].frozen) | |
def get_eff_yield(self): | |
""" | |
The effective yield, the number of ice blocks that can exist. | |
""" | |
eff_yield = self._size * (self._size - 1) | |
if self._size > 7: | |
eff_yield -= self._size | |
# If odd, avoid counting the center twice when using a cross pattern | |
if self._size % 2 == 1: | |
eff_yield += 1 | |
return eff_yield | |
def clear_screen(): | |
os.system('cls' if os.name == 'nt' else 'clear') | |
def update_screen(farm: IceFarm, ticks: int): | |
clear_screen() | |
print(farm) | |
print(size, farm.count(), farm.get_eff_yield(), ticks) | |
def simulate_generation(size: int, debug: bool = False) -> int: | |
farm = IceFarm(size) | |
eff_yield = farm.get_eff_yield() | |
ticks = 0 | |
if debug: | |
update_screen(farm, ticks) | |
last_count = curr_count = farm.count() | |
while curr_count < eff_yield: | |
ticks += 1 | |
farm.update() | |
curr_count = farm.count() | |
if debug and curr_count > last_count: | |
last_count = curr_count | |
update_screen(farm, ticks) | |
return ticks | |
# Print the field size, min, max, average, and median tick runtime | |
CHUNK_SIZE = 16 | |
# Each chunk has a 1 in 16th chance of being weather updated | |
WEATHER_UPDATE_CHANCE = 1 / 16 | |
RUN_COUNT = 30 | |
MIN_SIZE = 3 | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser( | |
description=""" | |
Simulates the generation of a Minecraft ice farm and outputs a comma-separated line for each size, minimum duration, | |
maximum duration, average duration, and median duration (all in ticks). If the size is greater than 7, than an X pattern is used for | |
the water sources; otherwise, only a single diagnal is used. | |
""", | |
epilog=""" | |
Copyright (c) 2021 Christopher Philabaum <christopher@philabaum.me> | |
This work is licensed under the terms of the MIT license. | |
For a copy, see <https://opensource.org/licenses/MIT>. | |
""", | |
) | |
parser.add_argument("-s", "--size", default=7, type=int, | |
help="The size to test against, or if '--increment' is set, run up to this size. Cannot be below {}," | |
" and defaults to 7.".format(MIN_SIZE)) | |
parser.add_argument("-i", "--increment", action="store_true", | |
help="Increments starting at a size of 3 up to '--size' set.") | |
parser.add_argument("-d", "--debug", action="store_true", | |
help="Run in debug mode, where the farm will be printed instead, and no parallelism is done.") | |
args = parser.parse_args() | |
max_size = args.size | |
increment = args.increment | |
debug = args.debug | |
if max_size < 3: | |
raise ValueError("Size must be at least {}.".format(MIN_SIZE)) | |
sizes = range(MIN_SIZE if increment else max_size, max_size + 1) | |
# If we're running in debug mode, we don't want multiprocessing, and only do one run for each | |
if debug: | |
for size in sizes: | |
simulate_generation(size, debug) | |
else: | |
with multiprocessing.Pool() as pool: | |
for size in sizes: | |
runs = list(pool.imap_unordered(simulate_generation, repeat(size, RUN_COUNT))) | |
print("{},{},{},{},{}".format(size, min(runs), max(runs), sum(runs) / len(runs), | |
list(sorted(runs))[len(runs) // 2])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment