from emis_funky_funktions import * from world import Terrain, World from PIL import Image from itertools import chain from typing import List, NamedTuple, Tuple def meters_to_millimeters(meters: float) -> int: """ Convert meters (floating point) to millimeters (int) >>> meters_to_millimeters(1.9333473e+02) 193334 >>> meters_to_millimeters(1.0) 1000 """ return int(1000 * meters) def read_single_elevation(dat: str) -> Result[int, str]: """ Read in a single elevation Expects a single, stringified floating point value, in meters, without a unit, e.g. "2.057e+02". If there is a parsing error, returns an error containing the input string. Otherwise, returns the elevation in millimeters. >>> read_single_elevation("1.9333473e+02") Ok(193334) >>> read_single_elevation("1.0") Ok(1000) >>> read_single_elevation("10.0m") Err('10.0m') """ return map_res( meters_to_millimeters, # fuck floating points try_(replace(dat), float, dat) ) def read_elevation_line(line: str) -> Result[Iterator[int], Tuple[int, str]]: """ Reads a line of elevations Each elevation should be in the format specified by `read_single_elevation`. Every element of the line is parsed and combined into an iterator, which is returned. If any parsing errors are encountered, the first error is returned. It takes the form of a tuple containing the column of the error (in terms of whitespace-deliniated words, not characters) and the string which could not be parsed. Recall from the definition of `read_single_elevation()` that the return value is an integer number of millimeters. As per the problem specification, the last five columns of the line are not read or parsed. Parsing errors in these columns therefor cannot be encountered, and any data in these columns will not be returned. >>> map_res(list, read_elevation_line("1.9e2 18e1 181 179.1 ignored 170.1 1.8e2 18i+2 20e1")) Ok([190000, 180000, 181000, 179100]) >>> map_res(list, read_elevation_line("1.9e2 18e 181 179.1 ignored 170.1 1.8e2 18i+2 20e1")) Err((2, '18e')) """ return sequence([ map_err( lambda err_text: (col_no + 1, err_text), read_single_elevation(elem) ) # As per spec, we drop the last five entries of the file. for (col_no, elem) in enumerate(line.split()[:-5]) ]) class UnparsableElevation(NamedTuple): "An elevation entry failed to be parsed as a floating point" line_no: int "The line on which the error occurred" col_no: int "The column (1-indexed number of whitespace deliniated words) on which the error occurred" invalid_entry: str "The text which failed to parse as a floating point value" def read_elevations(lines: Iterable[str]) -> Result[Iterator[int], UnparsableElevation]: """ Read an entire elevation file into a list This parses each line in of the file using `read_elevation_line()`. Errors encountered are reported using an `UnparsableElevation`. The resulting list is single-dimensional, but can be indexed arbitrarily, meaning for an input file with a constant number of columns per line, this data can be accessed as if it were a 2d square array. However, keep in mind that, as with `read_elevation_line()`, the last five columns of each line are dropped/ignored, and are therefor not present in the returned data. >>> map_res(list, read_elevations(''' ... 1 2 3 4 5 6 7 8 9e 10 ... 11 2.0 3e1 4 5 6 7 bwa 9 10 ... 11 2.0 3e0 4 5 ign err 8 9 10 ... '''.strip().split('\\n'))) Ok([1000, 2000, 3000, 4000, 5000, 11000, 2000, 30000, 4000, 5000, 11000, 2000, 3000, 4000, 5000]) >>> map_res(list, read_elevations(''' ... 1 2 3 4 5 6 7 8 9e 10 ... 11 ERR 3e1 4 5 6 7 bwa 9 10 ... 11 2.0 3e0 4 5 ign err 8 9 10 ... '''.strip().split('\\n'))) Err(UnparsableElevation(line_no=2, col_no=2, invalid_entry='ERR')) """ return map_res( chain.from_iterable, sequence([ map_err( lambda err: UnparsableElevation(line_no + 1, *err), read_elevation_line(line) ) for (line_no, line) in enumerate(lines) ]) ) def gb_to_terrain_type(green_blue: Tuple[int, int]) -> Option[Terrain]: """ Maps the green and blue components of a color to a terrain type. If no terrain type exists with that color, then `None` is returned. >>> gb_to_terrain_type((192, 0)) Some() >>> gb_to_terrain_type((192, 1)) is None True """ match green_blue: case (148, 18): return Some(Terrain.OPEN_LAND) case (192, 0): return Some(Terrain.ROUGH_MEADOW) case (255, 255): return Some(Terrain.EASY_FOREST) case (208, 60): return Some(Terrain.MEDIUM_FOREST) case (136, 40): return Some(Terrain.WALK_FOREST) case (73, 24): return Some(Terrain.BRUSH) case (0, 255): return Some(Terrain.WET) case (51, 3): return Some(Terrain.ROAD) case (0, 0): return Some(Terrain.FOOTPATH) case (0, 101): return Some(Terrain.OOB) case _: return None class UnrecognizedColor(NamedTuple): "A color in a provided map was not in the list of recognized colors" x: int "The x coordinate of the unidentifided color" y: int "The y coordinate of the unidentifided color" def load_gb_pixels_as_map(pixels: Iterable[Tuple[int, int]]) -> Result[Iterator[Terrain], UnrecognizedColor]: """ Interpret the green and blue bands of an image as pixels on a map This assumes that the map follows the specification laid out at https://cs.rit.edu/~jro/courses/intelSys/labs/orienteering/ Namely, the map should be exactly 395 pixels wide, and should follow the color conventions laid out in the Terrain Type table. This function is only interested in the green and blue channels of the image. Rather than pass a full pixel, it is only necessary to pass the green and blue color values. If an error is encountered, the returned `Err` type will contain the x and y coordinates of the pixel which could not be identified. """ return sequence([ note( UnrecognizedColor(pix_no % 395, pix_no // 395), gb_to_terrain_type(gb) ) for (pix_no, gb) in enumerate(pixels) ]) def load_image(path: str) -> Result[List[Terrain], UnrecognizedColor]: "Run `load_gb_pixels_as_map` on an image read from a path" with Image.open(path) as im: return load_gb_pixels_as_map(zip(im.getdata(1), im.getdata(2))) def load_elevations(path: str) -> Result[Iterator[int], UnparsableElevation]: "Run `read_elevations` on a text file read from a path" with open(path) as file: return read_elevations(file) def load_world_from_paths(map_path: str, elevation_path: str) -> Result[World, UnparsableElevation | UnrecognizedColor]: """ Read world information (terrain and elevation) from file paths. See `read_elevations` and `load_gb_pixels_as_map` for more information on the format that this data should be in. Note that for this method, you do not need to worry about the channels present in the terrain map, just that it align with the colors and size of the specified format. """ return bind_res( lambda terrain_data: map_res( lambda elevation_data: World((*zip(terrain_data, elevation_data),)), load_elevations(elevation_path) ), load_image(map_path) ) if __name__ == '__main__': import doctest doctest.testmod()