9
\$\begingroup\$

I am still pretty new to Python. After spending time on Code Wars and copying a handful of projects from Al Sweigart's Big Book of Small Python Projects, I wanted to build something completely from scratch and settled on a version of Battleship versus the computer. I have the code working as I intended, but would appreciate feedback on anywhere I'm not following best practices, and any other feedback you feel is worth sharing with a beginner Python programmer.

My intent was to base the code on classic Battleship, so 5 ships of length 5, 4, 3, 3, 2 that can only be placed vertically or horizontally on a 10x10 grid. I wanted the computer to make random guesses until it hits, and then decide "like a person." I decided to give players the choice between selecting a randomized board and placing their ships manually. And I wanted to validate user input. I believe I've achieved these things based on how the code runs for me. I've been through a couple iterations and made it about as efficient as I can figure out at my level, I think.

import random, sys, time
EMPTY = ' '
SHIP = 'S'
HIT = 'X'
MISS = 'O'
SHIPNAMES = {'Aircraft Carrier': 5, 'Battleship': 4, 'Cruiser': 3, 'Submarine': 3, 'Destroyer': 2}
ROWS = 'abcdefghij'
COLS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
def main():
 print('''
Ready to play Battleship? Your opponent has placed the ships listed
below on their 10x10 board in secret. Their lengths are as shown,
and they may be placed vertically or horizontally. You will attempt
to hit their ships by entering coordinates in L# format, e.g. A1. Your 
opponent will also be working to find and sink your ships, so choose
wisely!
The ship types:
 ''')
 for name, length in SHIPNAMES.items():
 print('{}: length {}'.format(name, length))
 print('''
To begin, decide whether you would like to (P)lace your own ships or
(C)hoose from a series of auto-generated ship placements.''')
 playerBoard = []
 while True:
 boardSetup = input(' Input P or C > ')
 if 'p' in boardSetup.lower():
 playerBoard = manualBoard()
 break
 elif 'c' in boardSetup.lower():
 playerBoard = chooseBoard()
 break
 print()
 time.sleep(1)
 print("Great! Let's get started. Your turn first!")
 print()
 time.sleep(1)
 aiBoard = getRandomBoard()
 playerGuesses = getEmptyBoard()
 aiGuesses = getEmptyBoard()
 playerLog = {'recentSink': [False], 'recentHit': '', 'recentHits': [], 'lastMove': (), 'ships': {}}
 aiLog = {'recentSink': [False], 'recentHit': '', 'recentHits': [], 'lastMove': (), 'ships': {}}
 while True:
 displayBoard(playerBoard,playerGuesses)
 print()
 playerGuesses, aiBoard, aiLog = makeMove(playerGuesses, aiBoard, aiLog, turn='Player')
 print()
 time.sleep(1)
 if aiLog['recentSink'][0] == True:
 print('Hit! You sank their {}!'.format(aiLog['recentSink'][-1]))
 print()
 time.sleep(1)
 if len(aiLog['recentSink']) == len(SHIPNAMES)+1:
 displayBoard(playerBoard, playerGuesses)
 print()
 print("That's the last one. Congratulations, you win!")
 sys.exit()
 elif aiLog['recentHits'] and aiLog['lastMove'] == aiLog['recentHits'][-1]:
 print('Hit! Nice job, keep it up!')
 else:
 print("Oof, that's a miss. Better luck next time!")
 print()
 time.sleep(1)
 print("It's the computer's turn. Let's see how it goes...")
 time.sleep(1)
 aiGuesses, playerBoard, playerLog = makeMove(aiGuesses, playerBoard, playerLog, turn='AI')
 print()
 if playerLog['recentSink'][0] == True:
 print('Hit! The computer sank your {}. Time for revenge?'.format(playerLog['recentSink'][-1]))
 if len(playerLog['recentSink']) == len(SHIPNAMES)+1:
 print("Oof, that's the last one. The computer wins, better luck next time!")
 response = input("Enter Y if you'd like to see the computer's board...")
 if 'y' in response.lower():
 displayBoard(aiBoard)
 sys.exit()
 elif playerLog['recentHits'] and playerLog['lastMove'] == playerLog['recentHits'][-1]:
 print('The computer hit your {} at {}{}. Ruh roh!'.format(playerLog['recentHit'],ROWS[playerLog['recentHits'][-1][0]].upper(),COLS[playerLog['recentHits'][-1][1]]))
 else:
 print("The computer attacked {}{} and missed. You're safe this time!".format(ROWS[playerLog['lastMove'][0]].upper(),COLS[playerLog['lastMove'][1]]))
 time.sleep(2)
def getEmptyBoard():
 board = []
 for row in range(10):
 board.append([])
 for col in range(10):
 board[row].append(EMPTY)
 return board
def getRandomBoard():
 board = getEmptyBoard()
 ships = [(name, length) for name, length in SHIPNAMES.items()]
 while ships:
 currentShip, shipLength = ships.pop(0)
 while True:
 row, col = random.randint(0,9), random.randint(0,9)
 if board[row][col] != EMPTY:
 continue
 direction = random.randint(1,4)
 if direction % 2 == 0:
 val = 1
 else:
 val = -1
 coords = []
 if direction < 3:
 for val in range(shipLength):
 if row+val < 0 or row+val >= len(board) or board[row+val][col] != EMPTY:
 break
 else:
 coords.append((row+val,col))
 if len(coords) != shipLength:
 continue
 else:
 for r, c in coords:
 board[r][c] = currentShip
 break
 else:
 for val in range(shipLength):
 if col+val < 0 or col+val >= len(board) or board[row][col+val] != EMPTY:
 break
 else:
 coords.append((row,col+val))
 if len(coords) != shipLength:
 continue
 else:
 for r, c in coords:
 board[r][c] = currentShip
 break
 return board
def chooseBoard():
 while True:
 board = getRandomBoard()
 displayBoard(board)
 print()
 print('Enter Y to keep, N to generate a new board...')
 keepboard = input('> ').lower()
 if 'y' in keepboard:
 return board
 elif 'q' in keepboard:
 sys.exit()
 time.sleep(1)
 print()
def manualBoard():
 while True:
 print()
 print("Let's set up your board. At any time you may enter Q to quit, R to restart.")
 print()
 time.sleep(1)
 ships = {key[0]: key for key in SHIPNAMES.keys()}
 board = getEmptyBoard()
 while ships:
 displayBoard(board)
 print()
 print('Choose which ship to place from below by entering its first letter.')
 for key, name in ships.items():
 print(' {}: '.format(key), end='')
 print(name)
 while True:
 choice = input('> ')[0].lower()
 if choice == 'r':
 break
 if choice == 'q':
 sys.exit()
 if ships.get(choice.upper()):
 break
 else:
 print()
 print('Invalid input, try again.')
 print()
 time.sleep(0.5)
 if choice == 'r':
 break
 ship = ships.pop(choice.upper())
 print()
 print('Choose the starting coordinate for this ship, e.g. A5.')
 row = ''
 col = ''
 start = ''
 while True:
 choice = input('> ').lower()
 if choice == 'r':
 break
 if choice == 'q':
 sys.exit()
 if len(choice) > 4:
 print()
 print('Input is too long, try again.')
 print()
 time.sleep(0.5)
 continue
 row = ''
 col = ''
 for char in choice:
 if char.isalpha():
 row += char
 elif char.isnumeric():
 col += char
 if row in ROWS and col in COLS:
 start, row, col = choice.upper(), ROWS.find(row), COLS.index(col)
 else:
 print()
 print('Input is invalid, try again.')
 print()
 time.sleep(0.5)
 continue
 if board[row][col] != EMPTY:
 print()
 print('You already have a ship there! Try again.')
 print()
 time.sleep(0.5)
 continue
 else:
 break
 if choice == 'r':
 break
 print()
 print('Choose the direction to place the ship from {}. (W)'.format(choice))
 print(' (A)(S)(D)')
 direction = ''
 while True:
 choice = input('> ')[0].lower()
 if choice == 'r':
 break
 if choice == 'q':
 sys.exit()
 if choice in 'wasd':
 if choice == 'w' and row - (SHIPNAMES[ship]-1) >= 0:
 clear = True
 for val in range(SHIPNAMES[ship]):
 if board[row-val][col] != EMPTY:
 clear = False
 break
 if clear:
 direction = 'w'
 break
 else:
 print()
 print('There is another ship in the way in that direction, try again.')
 print()
 time.sleep(0.5)
 continue
 elif choice == 's' and row + (SHIPNAMES[ship]-1) <= len(board):
 clear = True
 for val in range(SHIPNAMES[ship]):
 if board[row+val][col] != EMPTY:
 clear = False
 break
 if clear:
 direction = 's'
 break
 else:
 print()
 print('There is another ship in the way in that direction, try again.')
 print()
 time.sleep(0.5)
 continue
 elif choice == 'a' and col - (SHIPNAMES[ship]-1) >= 0:
 clear = True
 for val in range(SHIPNAMES[ship]):
 if board[row][col-val] != EMPTY:
 clear = False
 break
 if clear:
 direction = 'a'
 break
 else:
 print()
 print('There is another ship in the way in that direction, try again.')
 print()
 time.sleep(0.5)
 continue
 elif choice == 'd' and col + (SHIPNAMES[ship]-1) <= len(board):
 clear = True
 for val in range(SHIPNAMES[ship]):
 if board[row][col+val] != EMPTY:
 clear = False
 break
 if clear:
 direction = 'd'
 break
 else:
 print()
 print('There is another ship in the way in that direction, try again.')
 print()
 time.sleep(0.5)
 continue
 else:
 print()
 print('There is not enough room to place the ship in that direction, try again.')
 print()
 time.sleep(0.5)
 continue
 if choice == 'r':
 break
 wasd = {'w': 'up', 's': 'down', 'a': 'left', 'd': 'right'}
 print()
 print('You have chosen to place your {} starting from {} and going {}.'.format(ship, start, wasd[direction]))
 print('Enter Y to accept this and continue. Enter N to restart at the ship selection prompt.')
 choice = input('> ')[0].lower()
 if choice == 'q':
 sys.exit()
 if choice == 'r':
 break
 if choice != 'y':
 ships[ship[0]] = ship
 continue
 if direction in 'wa':
 inc = -1
 else:
 inc = 1
 for val in range(SHIPNAMES[ship]):
 if direction in 'ws':
 board[row+val*inc][col] = ship
 else:
 board[row][col+val*inc] = ship
 if choice == 'r':
 continue
 return board
 
def displayBoard(board,guesses=None):
 labels = []
 if guesses:
 for row in range(10):
 labels.extend(board[row])
 labels.extend(guesses[row])
 for i, space in enumerate(labels):
 if space != EMPTY:
 labels[i] = space[0]
 print('''
 PLAYER BOARD GUESSES
 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 A | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | A | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 B | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | B | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 C | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | C | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 D | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | D | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 E | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | E | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 F | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | F | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 G | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | G | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 H | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | H | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 I | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | I | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+
 J | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | J | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+---+'''.format(*labels))
 else:
 for row in range(10):
 labels.extend(board[row])
 for i, space in enumerate(labels):
 if space != EMPTY:
 labels[i] = space[0]
 print('''
 1 2 3 4 5 6 7 8 9 10
 +---+---+---+---+---+---+---+---+---+---+
 A | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 B | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 C | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 D | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 E | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 F | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 G | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 H | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 I | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+
 J | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |
 +---+---+---+---+---+---+---+---+---+---+'''.format(*labels))
def getPlayerMove(guesses):
 while True:
 print('Enter your guess as a row letter and column number, e.g. A10.')
 print('Enter Q to quit.')
 guess = input('> ').lower()
 if 'q' in guess:
 print('Thanks for playing!')
 sys.exit()
 if len(guess) > 4:
 print()
 print('Input is too long, try again.')
 print()
 time.sleep(0.5)
 continue
 row = ''
 col = ''
 for char in guess:
 if char.isalpha():
 row += char
 elif char.isnumeric():
 col += char
 if row in ROWS and col in COLS:
 row, col = ROWS.find(row), COLS.index(col)
 else:
 print()
 print('Input is invalid, try again.')
 print()
 time.sleep(0.5)
 continue
 if guesses[row][col] != EMPTY:
 print()
 print('You already guessed that! Try again.')
 print()
 time.sleep(0.5)
 continue
 else:
 return row, col
def getAIMove(guesses, log):
 if log['recentHits']:
 firstRow, firstCol = log['recentHits'][0]
 if len(log['recentHits']) > 1:
 recentRow, recentCol = log['recentHits'][-1]
 if recentRow == firstRow:
 options = []
 for num in [recentCol+1,recentCol-1,firstCol+1,firstCol-1]:
 if 0 <= num <= 9 and guesses[recentRow][num] == EMPTY:
 options.append(num)
 random.shuffle(options)
 if options:
 return recentRow, options[0]
 else:
 options = []
 for num in [recentRow+1,recentRow-1,recentRow+1,recentRow-1]:
 if 0 <= num <= 9 and guesses[num][recentCol] == EMPTY:
 options.append(num)
 random.shuffle(options)
 if options:
 return options[0], recentCol
 potentials = [(firstRow, firstCol+1), (firstRow, firstCol-1), (firstRow+1, firstCol), (firstRow-1, firstCol)]
 if len(log['recentHits']) > 1:
 for coord in log['recentHits']:
 newRow, newCol = coord
 potentials.extend([(newRow, newCol+1), (newRow, newCol-1), (newRow+1, newCol), (newRow-1, newCol)])
 options = []
 for pair in potentials:
 guessRow, guessCol = pair
 if pair not in log['recentHits'] and 0 <= guessRow <= 9 and 0 <= guessCol <= 9 and guesses[guessRow][guessCol] == EMPTY:
 options.append(pair)
 random.shuffle(options)
 if options:
 return options[0]
 while True:
 guessRow, guessCol = random.randint(0,9), random.randint(0,9)
 if guesses[guessRow][guessCol] == EMPTY:
 return guessRow, guessCol
def makeMove(guesses, board, log, turn):
 if turn == 'Player':
 row, col = getPlayerMove(guesses)
 else:
 row, col = getAIMove(guesses, log)
 log['lastMove'] = (row, col)
 if board[row][col] == EMPTY:
 guesses[row][col] = MISS
 log['recentSink'][0] = False
 if not log['recentHits']:
 unsunkShip = ''
 for ship, coords in log['ships'].items():
 if 0 < len(coords) < SHIPNAMES[ship]:
 unsunkShip = ship
 break
 if unsunkShip:
 log['recentHits'] = [log['ships'][unsunkShip][0]]
 log['recentHit'] = unsunkShip
 return guesses, board, log
 else:
 hitShip = board[row][col]
 guesses[row][col] = HIT
 board[row][col] = HIT
 if not log['ships'].get(hitShip):
 log['ships'][hitShip] = []
 log['ships'][hitShip].append((row, col))
 if SHIPNAMES[hitShip] == len(log['ships'][hitShip]):
 for coord in log['ships'][hitShip]:
 sunkRow, sunkCol = coord
 guesses[sunkRow][sunkCol] = hitShip[0]
 log['recentSink'][0] = True
 log['recentSink'].append(hitShip)
 unsunkShip = ''
 for ship, coords in log['ships'].items():
 if 0 < len(coords) < SHIPNAMES[ship]:
 unsunkShip = ship
 break
 if unsunkShip:
 log['recentHits'] = [log['ships'][unsunkShip][0]]
 log['recentHit'] = unsunkShip
 else:
 log['recentHits'] = []
 return guesses, board, log
 else:
 log['recentHits'].append((row, col))
 log['recentHit'] = hitShip
 log['recentSink'][0] = False
 return guesses, board, log
if __name__ == '__main__':
 main()
mdfst13
22.4k6 gold badges34 silver badges70 bronze badges
asked Jun 7, 2024 at 2:53
\$\endgroup\$
2
  • \$\begingroup\$ Very cool! But on the UX side, consider printing the previous turn's result after the new board (i.e., underneath it). As is, while we're in the middle of reading the previous outcome, it gets scrolled away to display the new board. \$\endgroup\$ Commented Jun 7, 2024 at 19:17
  • \$\begingroup\$ Thanks for the advice, that's a great point! \$\endgroup\$ Commented Jun 9, 2024 at 1:43

1 Answer 1

8
\$\begingroup\$

Use more vertical whitespace:

It seems as if the code was manually formatted. Consider using black or ruff to format the code according to PEP-8. This would also save your time.

Simpler to use f-strings:

for name, length in SHIPNAMES.items():
 print('{}: length {}'.format(name, length))

The second statement can be written simpler as:

print(f'{name}: length {length}')

Use more functions:

Currently, the main() function is too long and responsible for a lot more than it should be.

It is responsible for:

  • taking and validating input.
  • board selection.
  • the main game loop.

Consider defining a function to take input, another to select the board, and another that runs the main loop, and some more helper functions.

manualBoard() is too long as well. Consider defining helper functions to deal with each keypress separately.

Use a list comprehension:

getEmptyBoard() can be replaced with a simple one-liner:

return [[EMPTY for _ in range(10)] for _ in range(10)]

You're already using a similar comprehension getRandomBoard().

In getRandomBoard(), you can also simplify:

if direction % 2 == 0:
 val = 1
else:
 val = -1

to:

val = 1 if direction % 2 == 0 else -1

Good job on the if __name__ == '__main__': guard.

toolic
15.9k6 gold badges29 silver badges217 bronze badges
answered Jun 7, 2024 at 3:17
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Thank you for the feedback! I didn't know about the code formatting resources you shared and I look forward to using them. I also appreciate the simplifications you recommended, the guidance that f-strings are preferred, and the recommendation to out-source into more helper functions. I will keep all this in mind moving forward! \$\endgroup\$ Commented Jun 7, 2024 at 19:16

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.