top of page

Mastering Super Farmer Python Series with TDD: Tackling Foxes, Hounds, and Pairing Logic



pixelart of robot superfarmer
Robot super farmer

Introduction

Welcome back to our Super Farmer Python Series! In this part, we're delving into the intricate logic of foxes, hounds, and managing matching pairs of animals. Using Test-Driven Development (TDD), we'll break down these game mechanics step by step, transforming complex rules into manageable and testable code.

Throughout the series, we’ve embraced the RED-GREEN-REFACTOR cycle of TDD: write a failing test (RED), implement just enough code to pass (GREEN), and then clean up and optimize the code (REFACTOR). This method helps us manage game complexity while ensuring that every feature works as expected before moving on to the next.

If you're just joining us, be sure to check out the earlier parts linked below to understand how we’ve been building the game from scratch.


TDD and Super Farmer: Simplifying Main Herd Management

During development, I initially managed the main herd as a list of animal objects, such as self.main_herd = [Rabbit(60), Sheep(24)...]. However, this approach quickly became cumbersome, requiring extra logic to handle transfers and updates separately for both the main herd and the players.


The Refactor: Treating the Main Herd as a Player

It became clear that the best way to simplify things was to treat the main herd as a Player instance, just like the players themselves. This unified the logic and made herd management much easier.

python```
self.main_herd = Player(name='Main Herd', index=99)
self.main_herd.herd = {"Rabbit": 60, "Sheep": 24, "Pig": 12,...}
```

Now, instead of needing custom logic for the main herd, we could use the same transfer methods for both players and the main herd, making the game logic cleaner and more consistent.


Why the Refactor Was Necessary

  • Code Complexity: Managing two separate structures—one for the main herd and one for players—introduced unnecessary complexity. Every transfer of animals required custom handling for each.

  • Transfer Logic: By making the main herd a Player, we could reuse the transfer_to and transfer_from methods, significantly reducing redundancy and making the code easier to maintain.

  • Future-proofing: This refactor allows for more flexibility if the game mechanics need to be expanded or new features added later on. The code is now modular, and animal transfers happen seamlessly across the board.


This refactor aligns perfectly with the TDD refactor phase, where we take a step back, look at the code structure, and optimize it to ensure future scalability.


Handling Sly Foxes

Our first challenge is managing what happens when a fox is rolled. In Super Farmer, if a player rolls a fox, they lose all their rabbits unless they have a foxhound.


Step 1: Writing the Test (RED)

We'll start with a test to ensure that when a fox is rolled, the player loses their rabbits unless they have a foxhound. The test is written to fail initially.

python```
def test_given_no_foxhound_when_player_rolls_fox_then_lose_rabbits(self): 		
	"""Test if player loses rabbits when a fox is rolled and there’s no foxhound.""" 
	# Setup omitted for brevity 
	game_manager.process_dice(test_player, "Rabbit", "Fox") 
	player_herd = test_player.get_herd() 		
	self.assertEqual(player_herd["Rabbit"], 0)
```

Step 2: Implementing the Logic (GREEN)

Now that the test fails (as expected), we implement the logic to ensure that the rabbits are lost when a fox is rolled. If the player has a foxhound, only the foxhound is lost.

python```
if result_green == "Fox" or result_red == "Fox": 	
	if current_player.get_herd()["Foxhound"] > 0: 	
		current_player.update_herd({"Foxhound": current_player.get_herd()
		["Foxhound"] - 1}) 
	else: 
		return_amount = current_player.get_herd()["Rabbit"] 
	current_player.update_herd({"Rabbit": 0}) 	
	self.main_herd.herd["Rabbit"] += return_amount
```

Step 3: Refactoring the Code (REFACTOR)

After the logic works and passes the test, we clean up the code. Since the main herd is now a Player object, we can easily update the herd's counts without additional complexity.


Foxhound Protection

Now, let’s extend the fox logic to include foxhound protection. If the player rolls a fox and has a foxhound, only the foxhound is lost, and the rest of the herd remains intact.


Step 1: Writing the Test (RED)

python```
def test_given_foxhound_when_player_rolls_fox_then_lose_foxhound(self): 	
	"""Test if player loses only the foxhound when a fox is rolled.""" 
	# Setup omitted for brevity 
	game_manager.process_dice(test_player, "Rabbit", "Fox") 
	player_herd = test_player.get_herd() 	
	self.assertEqual(player_herd["Foxhound"], 0) 
	self.assertGreater(player_herd["Rabbit"], 0) # Herd should be intact
```

Step 2: Adding the Foxhound Logic (GREEN)

python```
if result_green == "Fox" or result_red == "Fox": 	
	if current_player.get_herd()["Foxhound"] > 0: 
	current_player.update_herd({"Foxhound": current_player.get_herd()	
	["Foxhound"] - 1}) else: return_amount = current_player.get_herd()
	["Rabbit"] current_player.update_herd({"Rabbit": 0}) 
	self.main_herd.herd["Rabbit"] += return_amount
```

The logic here ensures that if a player has a foxhound, only that foxhound is lost, keeping the herd safe from the fox.


Pairing Up: Rolling Matching Animals

Next up, let’s deal with rolling pairs. When a player rolls a matching pair of animals, their herd should grow by the number of pairs they already have.


Step 1: Writing the Test (RED)

python```
def test_given_matching_pair_herd_is_updated(self): 
	"""Test if herd is updated when a player rolls a matching pair.""" 
	# Setup omitted for brevity 
	game_manager.process_dice(test_player, "Rabbit", "Rabbit") 	
	player_herd = test_player.get_herd() 
	self.assertEqual(player_herd["Rabbit"], 6) # Assuming player had 4 	
	rabbits initially
```


Step 2: Adding the Logic (GREEN)

python```
if result_green == result_red:
	current_count = current_player.get_herd()[result_green] 
	green_pairs = current_player.get_herd()[result_green] // 2 
	self.main_herd.herd[result_green] -= green_pairs 
	current_player.update_herd({result_green: current_count + 	
	green_pairs})
```

This logic ensures that rolling a pair of animals increases the player's herd, while keeping the main herd count updated.


Ensuring Main Herd Sanity

Lastly, let’s add some safeguards to prevent the main herd from going into negative numbers. If a player’s herd grows too fast, it shouldn’t cause issues with the bank of animals (main herd).


Step 1: Writing the Test (RED)

python```
def test_given_low_main_herd_when_player_rolls_pair_then_max_count_not_below_zero(self): 
	"""Test if the main herd count doesn’t go below zero when a low main 			
	herd and matching animals are rolled.""" 
	# Setup omitted for brevity 
	game_manager.process_dice(test_player, "Rabbit", "Rabbit") 
	final_main_herd_count = game_manager.main_herd.herd["Rabbit"] 
	self.assertGreaterEqual(final_main_herd_count, 0)
```

Step 2: Adding the Safeguard (GREEN)

python```
if result_green == result_red: 
	if self.main_herd.herd[result_green] - green_pairs >= 0: 
		self.main_herd.herd[result_green] -= green_pairs 
		current_player.update_herd({result_green: current_count + 		
		green_pairs})
``

Final Process for Handling Dice Rolls

The final process_dice method has been refactored for simplicity. With the main herd as a Player, handling foxes, wolves, and pairs becomes more straightforward.

python```
def process_dice(self, current_player: Player, result_green: str, result_red: str): 
	"""Process dice roll and transfer animals between the main herd and 	
	the current player.""" 
	if result_green == "Fox" or result_red == "Fox": 
		if self.handle_fox(current_player): 
			return 
		if result_green == "Wolf" or result_red == "Wolf": 	
			if self.handle_wolf(current_player): 
			return 
	
	# Matched dice: process pairs 
	if result_green == result_red: pairs = 
	current_player.get_herd().get(result_green, 0) // 2 
		available_animals = self.main_herd.get_herd().get(result_green, 
	0) 
		transfer_count = min(available_animals, max(pairs, 1)) 
		self.main_herd.transfer_to(current_player, result_green, 
	transfer_count) 
	else: 
		if current_player.get_herd().get(result_green, 0) % 2 != 0: 
			if self.main_herd.get_herd().get(result_green, 0) > 0: 
				self.main_herd.transfer_to(current_player, result_green, 
	1) 
			if self.main_herd.get_herd().get(result_green, 0) > 0: 
				self.main_herd.transfer_to(current_player, result_green, 
	1) 
			if self.main_herd.get_herd().get(result_red, 0) > 0: 
				self.main_herd.transfer_to(current_player, result_red, 
	1) 
	print(f"Processing dice roll for {current_player.name}. Herd: 	
	{current_player.get_herd()}")
```

Conclusion

In this leg of the Super Farmer Python Series, we explored how to use TDD to handle:

  • Sly Foxes: Deducting rabbits unless protected by a foxhound.

  • Foxhound Protection: Losing only the foxhound if present, leaving the herd intact.

  • Pairing Up: Updating the herd when matching animals are rolled.

  • Main Herd Refactor: Simplifying the main herd by treating it as a Player, allowing us to manage the bank of animals more efficiently.

  • Main Herd Sanity: Ensuring the main herd doesn’t go into negative numbers.


By embracing TDD, we’ve transformed complex game mechanics into clean, testable code. With each RED-GREEN-REFACTOR cycle, we made sure the logic worked exactly as intended before moving forward. Stay tuned for the next part of the series, where we’ll dive into more game logic and continue building on these core mechanics!


Call to Action

Have you used TDD for game development before? What’s been your experience tackling complex logic with testing-first methodologies? Share your thoughts and any tips you have in the comments below!

Comments


Subscribe to QABites newsletter

Thanks for submitting!

  • Twitter
  • Facebook
  • Linkedin

© 2023 by QaBites. Powered and secured by Wix

bottom of page