BattleShip Part 8: Addressing boundary issue

Running the project as-is, I noticed a problem with the output.

$ python3 main.py 
Your Move > show
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]

There are 10 rows but only 9 columns.

Also, my validation check is too strict because it only allows a single letter for the attack command. 1-9. So I’ll relax this to allow any numeric ending.

valid_attack_pattern = r'(^[a-z]{1})([0-9]+$)' 

This causes a new issue since now I can enter coordinates outside the board.

 $ python3 main.py
Your Move > show
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
[ ,  ,  ,  ,  ,  ,  ,  ,  ]
Your Move > a10
Ship missed!
Your Move > a11
Ship missed!

I’ll update the attack method of the game to return a more structured type instead of just a boolean.

class AttackResult(Enum):
    SUCCESS = 1
    MISSED = 2                            
    INVALID = 3 

Then I’ll update the attack method to return the attack result.

    def attack(self, row, col):
        coord = (row, col)

        if not self._on_board(row, col):
            return AttackResult.INVALID

        # already attacked that position
        if coord in self.attacked_coords:
            return AttackResult.MISSED

        self.attacked_coords.add(coord)
        if coord in self.ship_coords:
            return AttackResult.SUCCESS

        return AttackResult.MISSED

    def _on_board(self, row, col):
        row_index = letter_to_index(row) + 1
        col_index = int(col)
        return (
            col_index >= 1 and \
            col_index <= self.col_length and \
            row_index >= 1 and \
            row_index <= self.row_length
        )

Now the _attack method in GameCli can properly handle the different cases.

    def _attack(self, row, col):                                                                                                                                                                                                              
        result = self.game.attack(row, col)                                                                                                                                                                                                   
        if result == AttackResult.SUCCESS:                                                                                                                                                                                                    
            print('Ship hit!')                                                                                                                                                                                                                
        elif result == AttackResult.MISSED:                                                                                                                                                                                                   
            print('Ship missed!')                                                                                                                                                                                                             
        elif result == AttackResult.INVALID:                                                                                                                                                                                                  
            print('Invalid coordinates!')                                                                                                                                                                                                     
        else:                                                                                                                                                                                                                                 
            raise Exception('There is an unhandled attack type') 

One thing I found a little painful about doing this is in Game, I’ve been using raw user-provided input for keeping track of things like coordinates i.e. (“A”, “1”). It also requires the ship_placer to have to translate its output into this type of format.

I’ll refactor Game to track things with a zero-based indexing instead of these raw values which will keep things a bit more consistent and I don’t have to translate between 0-based and 1-based indexing in a bunch of places.

I’ll define a function that will translate the user input into integer coordinates.

def to_coordinates(row, col):
    '''Translates user provided input to zero index based matrix coordates: e.g. A1 -> (0, 0).'''
    row_index = letter_to_index(row)
    col_index = int(col) - 1
    return (row_index, col_index)

Then I can remove all this +1 business and keep things simpler.

    def get_board_layout(self):
        return [
            [self._get_coord_status(row_index, col_index) for col_index in range(self.col_length) ]
            for row_index in range(self.row_length)
        ]

    def _get_coord_status(self, row, col):
        coord = (row, col)

        if coord in self.attacked_coords and coord in self.ship_coords:
            return CoordInfo(CoordStatus.SHIP_HIT, self.ship_coords[coord])

        if coord in self.attacked_coords:
            return CoordInfo(CoordStatus.SHIP_MISSED, None)

        return CoordInfo(CoordStatus.EMPTY, None)

    def _on_board(self, coord):
        (row_index, col_index) = coord
        return (
            col_index >= 0 and \
            col_index < self.col_length and \
            row_index >= 0 and \
            row_index < self.row_length
        )

Finally I update ship_placer to just return the integer-based coordinates. Since I changed the return type, I’ll also need to update the test.

Project changeset.

One response to “BattleShip Part 8: Addressing boundary issue”

  1. […] BattleShip Part 8: Addressing boundary issue […]

    Like

Leave a comment