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()
-
\$\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\$tdy– tdy2024年06月07日 19:17:16 +00:00Commented Jun 7, 2024 at 19:17
-
\$\begingroup\$ Thanks for the advice, that's a great point! \$\endgroup\$Jessica Monnier– Jessica Monnier2024年06月09日 01:43:26 +00:00Commented Jun 9, 2024 at 1:43
1 Answer 1
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.
-
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\$Jessica Monnier– Jessica Monnier2024年06月07日 19:16:10 +00:00Commented Jun 7, 2024 at 19:16
You must log in to answer this question.
Explore related questions
See similar questions with these tags.