Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 2023-Oct-18/Dave/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
30 changes: 30 additions & 0 deletions 2023-Oct-18/Dave/direction.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from a C style background I think this enum does too much but a python/java/not C/C++ engineer would probably think it's fine.
(See a later comment about this enum where it is used in main.py)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be interested to hear a suggestion for an alternative. I definitely like the dot syntax. I had previously thought about using a named tuple, but something about that feels wrong, since there will only ever be one of these with fixed values and it seems odd to have the option to instantiate something that will only ever be one version of itself.

Open to suggestions, though.

Copy link
Contributor Author

@JustCallMeRay JustCallMeRay Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The C solution is macros (not in python thank god) The solution in other languages might be an enum or a hashmap. C++ has compile time constants (constexpr) so you can create a constexpr map or even a runtime map as everything is so fast it's fine.

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)
14 changes: 14 additions & 0 deletions 2023-Oct-18/Dave/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class FailedToGenerateWordSearchError(Exception):
...


class FailedToPlaceAllWordsError(Exception):
...


class NoLegalPlacementsError(Exception):
...


class GridOverflowError(Exception):
...
101 changes: 101 additions & 0 deletions 2023-Oct-18/Dave/main.py
Original file line number Diff line number Diff line change
@@ -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__":
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer

if __name__ != "__main__":
    raise someError("Running main as module is not supported")
main()

or

if __name__ != "__main__":
     print("Running main as module is not supported")
     exit(not_zero)
main()

As they force the correct usage on the user but it's more just style so up to you. These (not the main() call) can be added to the top of the file instead of the bottom to make you encounter this error quicker (and python has to do less compiling)

Copy link
Collaborator

@DaveDangereux DaveDangereux Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Would this influence testing?

As in would this not raise an error if I imported main for testing? Might be completely off-target here, but keen to know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't require main in my tests as it is normally very small and a full end to end integration test you may want to do with command line instead.
Other languages have one entry point called main so you can't EVER compile it in (unless you change the entry point which is kinda hacky)

main()
16 changes: 16 additions & 0 deletions 2023-Oct-18/Dave/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
15 changes: 15 additions & 0 deletions 2023-Oct-18/Dave/placement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from point import Point
from direction import Direction


@dataclass
class Placement:
position: Point
direction: Direction
Comment on lines +8 to +9
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the "prefer composition over inheritance"!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a design pattern thing here is a good explanation but it's a very common phrase.


# This allows us to unpack a placement with the syntax:
# position, direction = placement
def __iter__(self):
yield self.position
yield self.direction
Loading