Interactions
Hello, loves!
Let’s figure out the new interaction feature, test, and do it. Success but with a reset and a general feeling of clumsiness.
I think that the behavior we want is:
- When moving into a new cell, run the interactions and move if they all return true. (This is the current behavior.)
- If for any reason the move to a new cell is not allowed, remain where you are, run the interactions (again) there, ignoring the boolean results.
How do things work now? Thanks for asking, I was kind of wondering myself.
class Dungeon:
def move_player(self, direction):
new_cell = self.player_cell.attempt_move(direction)
if new_cell != self.player_cell:
self.place_player_at(new_cell)
def place_player_at(self, cell):
interactor = Interactor(self, cell)
if all(interactor.execute(item) for item in list(self.contents_at(cell))):
self.player_cell = cell
Looks like we should inspect the Interactor:
class Interactor:
def __init__(self, dungeon, cell):
self.dungeon = dungeon
self.cell = cell
def execute(self, item):
return item.interact_with_player(self)
def publish(self, event, caller_id, *args, **kwargs):
self.dungeon.publish(event, caller_id, *args, **kwargs)
def receive_content(self, item):
self.dungeon.receive_content_from_cell(item, self.cell)
I think it might be wise to have a test for this. There are no direct tests for interactor, which is easy to imagine, since it has no conditional behavior, it’s just a convenient place to stand. Almost makes me wonder why I wrote it at all.
Let’s see if there are any tests about this. Here are two that fail when I patch an assert False into Interactor:
def test_contents_to_player(self):
layout = DungeonLayout(10, 10)
room_cell = Cell(5, 5)
room = Room([room_cell])
layout.add_room(room)
dungeon = Dungeon(layout)
factory = ContentFactory()
item_1 = factory.receivable(name="treasure", resource='none', scale=0.5)
shape = [(0.33, 0.66), (0.66, 0.66), (0.5, 0.33)]
item_2 = factory.receivable(name="more treasure", resource='none', scale=0.5)
dungeon.place_content_at(room_cell,item_1)
dungeon.place_content_at(room_cell,item_2)
dungeon.place_player_at(room_cell)
assert item_1 in dungeon.inventory()
assert item_2 in dungeon.inventory()
assert dungeon.contents_at(room_cell) == []
def test_cannot_move_if_content_refuses(self):
layout = DungeonLayout(10, 10)
cell = Cell(5, 5)
content_cell = Cell(6,5)
room = Room([cell, content_cell])
layout.add_room(room)
dungeon = Dungeon(layout)
dungeon.place_player_at(cell)
content = ContentReturnsFalse()
dungeon.place_content_at(content_cell,content)
dungeon.move_player(Direction.east)
new_cell = dungeon.player_cell
assert new_cell == cell
class ContentReturnsFalse:
def run(self, dungeon):
pass
def interact_with_player(self, dungeon):
return False
I think we can repurpose those into two more tests, which should fail until the new scheme works. This is nasty but valid, failing on the final assertion:
def test_content_runs_again_if_cannot_move(self):
layout = DungeonLayout(10, 10)
origin = Cell(5, 5)
target = Cell(6,5)
origin_room = Room([origin])
layout.add_room(origin_room)
target_room = Room([target])
layout.add_room(target_room)
dungeon = Dungeon(layout)
dungeon.place_player_at(origin)
content = ContentFactory().receivable(name="treasure", resource='none', scale=0.5)
dungeon.place_content_at(origin,content)
dungeon.move_player(Direction.east)
assert dungeon.player_cell == origin
assert content in dungeon.inventory()
With that in play, let’s see if we fix up the placement logic. I plan to make it work and then see if it can be made better.
My attempt breaks six tests. This is not an improvement. I need to revert.
OK, we are here, with the one test above failing, everything else green:
def move_player(self, direction):
new_cell = self.player_cell.attempt_move(direction)
if new_cell != self.player_cell:
self.place_player_at(new_cell)
def place_player_at(self, cell):
interactor = Interactor(self, cell)
if all(interactor.execute(item) for item in list(self.contents_at(cell))):
self.player_cell = cell
One issue is that people expect place_player_at to do that and I think I’d like to just know if all the content approved. Let’s extract two methods. One might suffice but we’ll go the whole way for clarity.
def move_player(self, direction):
new_cell = self.player_cell.attempt_move(direction)
if new_cell != self.player_cell:
self.place_player_at(new_cell)
def place_player_at(self, cell):
if self.contents_allow_move(cell):
self.set_player_position(cell)
def contents_allow_move(self, cell):
interactor = Interactor(self, cell)
move_is_allowed = all(interactor.execute(item) for item in list(self.contents_at(cell)))
return move_is_allowed
def set_player_position(self, cell):
self.player_cell = cell
Tests same as before. Make a save point. Now try (again) to put in the logic we need:
class Dungeon:
def move_player(self, direction):
new_cell = self.player_cell.attempt_move(direction)
if new_cell != self.player_cell and self.contents_allow_move(new_cell):
self.set_player_position(new_cell)
else:
self.contents_allow_move(self.player_cell)
I had to modify the new test to use set_player_position:
def test_content_runs_again_if_cannot_move(self):
layout = DungeonLayout(10, 10)
origin = Cell(5, 5)
target = Cell(6,5)
origin_room = Room([origin])
layout.add_room(origin_room)
target_room = Room([target])
layout.add_room(target_room)
dungeon = Dungeon(layout)
dungeon.set_player_position(origin)
content = ContentFactory().receivable(name="treasure", resource='none', scale=0.5)
dungeon.place_content_at(origin,content)
dungeon.move_player(Direction.east)
assert dungeon.player_cell == origin
assert content in dungeon.inventory()
This test is exercising the else: in the method, I believe. Yes. So are a few others.
Now the test for being refused entry because the contents don’t like you.
def test_content_runs_again_if_content_refuses_entry(self):
layout = DungeonLayout(10, 10)
dungeon = Dungeon(layout)
origin = Cell(5, 5)
target = Cell(6,5)
room = Room([origin, target], '1')
layout.add_room(room)
dungeon.set_player_position(origin)
deny_entry = ContentReturnsFalse()
dungeon.place_content_at(target,deny_entry)
item_1 = ContentFactory().receivable(name="treasure", resource='none', scale=0.5)
dungeon.place_content_at(origin,item_1)
assert item_1 not in dungeon.inventory()
dungeon.move_player(Direction.east)
assert dungeon.player_cell == origin
assert item_1 in dungeon.inventory()
Both tests are passing with the code above. This surprises me a bit, because my small brain thought it would require another check but as things stand it does not. This pleases me, but my confusion makes me wonder if things are quite right. Anyway I think I’ll back away slowly.
Summary
Although everything has turned out fine in the end, I felt a lot of fumbling and stumbling, very unlike this morning, when I seemed to be tripping merrily along with no problems.
And I really hate those tests with a passion. Maybe I can engage Hill or someone to help me see how something smaller and more direct could have been done. At this moment, and for quite a few moments prior to now, I just don’t see how to avoid this kind of “story test” that sets up a massive scenario and then checks results. Maybe I can learn something.
But for now, we have our new feature, and I think next time we might just put it into play. We could almost do it now, but I’m tired and want to go read.
A decent outcome, a very small change to get what we needed, and I’m used to fumbling sometimes, so that’s OK too.
See you next time!