diff --git a/2023-Oct-18/Dave/.gitignore b/2023-Oct-18/Dave/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/2023-Oct-18/Dave/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/2023-Oct-18/Dave/direction.py b/2023-Oct-18/Dave/direction.py new file mode 100644 index 0000000..153331a --- /dev/null +++ b/2023-Oct-18/Dave/direction.py @@ -0,0 +1,30 @@ +from enum import Enum, EnumMeta +from point import Point + + +class DirectValueMeta(EnumMeta): + # This allows us to unpack enum members directly, in the format: + # row, col = Direction.RIGHT + def __getattribute__(cls, name): + value = super().__getattribute__(name) + if isinstance(value, cls): + return value.value + else: + return value + + # This allows us to iterate through enum members in a for loop and access + # the Point value directly + def __iter__(cls): + for value in super().__iter__(): + yield value.value + + +class Direction(Enum, metaclass=DirectValueMeta): + RIGHT = Point(0, 1) + DOWN = Point(1, 0) + UP = Point(-1, 0) + LEFT = Point(0, -1) + UP_LEFT = Point(-1, -1) + UP_RIGHT = Point(-1, 1) + DOWN_LEFT = Point(1, -1) + DOWN_RIGHT = Point(1, 1) diff --git a/2023-Oct-18/Dave/exceptions.py b/2023-Oct-18/Dave/exceptions.py new file mode 100644 index 0000000..e89c7f1 --- /dev/null +++ b/2023-Oct-18/Dave/exceptions.py @@ -0,0 +1,14 @@ +class FailedToGenerateWordSearchError(Exception): + ... + + +class FailedToPlaceAllWordsError(Exception): + ... + + +class NoLegalPlacementsError(Exception): + ... + + +class GridOverflowError(Exception): + ... diff --git a/2023-Oct-18/Dave/main.py b/2023-Oct-18/Dave/main.py new file mode 100644 index 0000000..2b4f1f1 --- /dev/null +++ b/2023-Oct-18/Dave/main.py @@ -0,0 +1,101 @@ +import random +from copy import deepcopy +from word_grid import WordGrid +from point import Point +from direction import Direction +from placement import Placement +from exceptions import ( + FailedToGenerateWordSearchError, + FailedToPlaceAllWordsError, + NoLegalPlacementsError, + GridOverflowError, +) + + +def main(): + word_list = ["PYTHON", "DOJO", "CODEHUB", "BRISTOL"] + + try: + word_search = generate_word_search(rows=6, cols=9, word_list=word_list) + print(word_search) + print("Find these words:") + print(", ".join(word_list)) + except FailedToGenerateWordSearchError: + print("Failed to generate word search.") + exit(1) + + +def generate_word_search(rows: int, cols: int, word_list: list[str]) -> WordGrid: + word_grid = WordGrid(rows, cols) + + attempts = 0 + max_attempts = 10 + + while attempts < max_attempts: + word_grid.initialise_grid() + try: + filled_word_search = place_words(word_grid, word_list) + filled_word_search.fill_blank_space() + return filled_word_search + except FailedToPlaceAllWordsError: + attempts += 1 + else: + raise FailedToGenerateWordSearchError() + + +def place_words(word_grid: WordGrid, word_list: list[str]) -> WordGrid: + word_search = deepcopy(word_grid) + + for word in word_list: + try: + placements = get_all_legal_placements_for_word(word_search, word) + position, direction = random.choice(placements) + word_search.write_line(position, direction, word) + except NoLegalPlacementsError: + raise FailedToPlaceAllWordsError() + + return word_search + + +def get_all_legal_placements_for_word( + word_grid: WordGrid, word: str +) -> list[Placement]: + legal_placements = [] + + # Iterate through all possible grid locations and orientations + for row_index, row in enumerate(word_grid.grid): + for col_index, col in enumerate(row): + for direction in Direction: + position = Point(row_index, col_index) + + line_can_be_written = word_grid.is_valid_line( + position, direction, len(word) + ) + if not line_can_be_written: + continue + + target_line = word_grid.read_line( + position, direction, len(word)) + line_can_be_placed = is_legal_placement( + target_line=target_line, line_to_write=word + ) + if not line_can_be_placed: + continue + + legal_placements.append(Placement(position, direction)) + + if len(legal_placements) == 0: + raise NoLegalPlacementsError() + else: + return legal_placements + + +def is_legal_placement(target_line: str, line_to_write: str) -> bool: + for target_char, char_to_write in zip(target_line, line_to_write): + if (char_to_write != target_char) and (target_char != " "): + return False + return True + + +if __name__ == "__main__": + main() diff --git a/2023-Oct-18/Dave/package.json b/2023-Oct-18/Dave/package.json new file mode 100644 index 0000000..8a81d93 --- /dev/null +++ b/2023-Oct-18/Dave/package.json @@ -0,0 +1,16 @@ +{ + "name": "Dave", + "version": "0.0.2", + "description": "", + "main": "main.py", + "scripts": { + "start": "nodemon main.py", + "test": "pytest" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/2023-Oct-18/Dave/placement.py b/2023-Oct-18/Dave/placement.py new file mode 100644 index 0000000..ce3f6ab --- /dev/null +++ b/2023-Oct-18/Dave/placement.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from point import Point +from direction import Direction + + +@dataclass +class Placement: + position: Point + direction: Direction + + # This allows us to unpack a placement with the syntax: + # position, direction = placement + def __iter__(self): + yield self.position + yield self.direction diff --git a/2023-Oct-18/Dave/pnpm-lock.yaml b/2023-Oct-18/Dave/pnpm-lock.yaml new file mode 100644 index 0000000..2f5a95b --- /dev/null +++ b/2023-Oct-18/Dave/pnpm-lock.yaml @@ -0,0 +1,239 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + nodemon: + specifier: ^3.0.1 + version: 3.0.1 + +packages: + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /debug@3.2.7(supports-color@5.5.0): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nodemon@3.0.1: + resolution: {integrity: sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.5.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true diff --git a/2023-Oct-18/Dave/point.py b/2023-Oct-18/Dave/point.py new file mode 100644 index 0000000..156f3e3 --- /dev/null +++ b/2023-Oct-18/Dave/point.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + + +@dataclass +class Point: + row: int + col: int + + # This allows us to unpack a point with the syntax: + # row, col = point + def __iter__(self): + yield self.row + yield self.col diff --git a/2023-Oct-18/Dave/pyproject.toml b/2023-Oct-18/Dave/pyproject.toml new file mode 100644 index 0000000..5a34bea --- /dev/null +++ b/2023-Oct-18/Dave/pyproject.toml @@ -0,0 +1,4 @@ +[tool.pytest.ini_options] +pythonpath = [ + "." +] diff --git a/2023-Oct-18/Dave/requirements.txt b/2023-Oct-18/Dave/requirements.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/2023-Oct-18/Dave/requirements.txt @@ -0,0 +1 @@ +pytest diff --git a/2023-Oct-18/Dave/word_grid.py b/2023-Oct-18/Dave/word_grid.py new file mode 100644 index 0000000..e1f5e4e --- /dev/null +++ b/2023-Oct-18/Dave/word_grid.py @@ -0,0 +1,95 @@ +import random +import string +from point import Point +from direction import Direction +from exceptions import GridOverflowError + + +class WordGrid: + def __init__(self, rows: int, cols: int): + self.rows = rows + self.cols = cols + self.initialise_grid() + + def __str__(self): + return self.get_stringified_word_grid() + + def initialise_grid(self): + grid = [] + + for row in range(self.rows): + grid_row = [] + for col in range(self.cols): + grid_row.append(" ") + grid.append(grid_row) + + self.grid = grid + + def get_stringified_word_grid(self) -> str: + output = "" + for row in self.grid: + row_string = " ".join(row) + output += f"{row_string}\n" + return output + + def is_valid_line(self, position: Point, direction: Direction, length: int) -> bool: + initial_row_index = position.row + initial_col_index = position.col + max_row_index = len(self.grid) - 1 + max_col_index = len(self.grid[0]) - 1 + + # A single character would have length 1 but no motion, hence multiplying + # the direction amount by length - 1 + total_row_motion = direction.row * (length - 1) + total_col_motion = direction.col * (length - 1) + + final_char_row_index = initial_row_index + total_row_motion + final_char_col_index = initial_col_index + total_col_motion + + if ( + final_char_row_index > max_row_index + or final_char_row_index < 0 + or final_char_col_index > max_col_index + or final_char_col_index < 0 + ): + return False + + return True + + def read_line(self, position: Point, direction: Direction, length: int) -> str: + if not self.is_valid_line(position, direction, length): + raise GridOverflowError() + + result = "" + + current_row, current_col = position + next_row, next_col = direction + + for i in range(length): + result += self.grid[current_row][current_col] + current_row += next_row + current_col += next_col + + return result + + def write_line(self, position: Point, direction: Direction, data: str) -> bool: + if not self.is_valid_line(position, direction, len(data)): + raise GridOverflowError() + + current_row, current_col = position + next_row, next_col = direction + + for char in data: + self.grid[current_row][current_col] = char + current_row += next_row + current_col += next_col + + return True + + def fill_blank_space(self): + for row_index, row in enumerate(self.grid): + for col_index, char in enumerate(row): + if char == " ": + self.grid[row_index][col_index] = random.choice( + string.ascii_uppercase + )