I started learning Python a few weeks ago and coded this "Connect 4" game.
What could I have done better? How efficient is this code and how could I improve on that?
from collections import defaultdict
from termcolor import colored
from time import sleep
class Board:
def __init__(self):
self.symbol = 0
# board MAIN horizontal
self.row1 = [0, 0, 0, 0, 0, 0, 0]
self.row2 = [0, 0, 0, 0, 0, 0, 0]
self.row3 = [0, 0, 0, 0, 0, 0, 0]
self.row4 = [0, 0, 0, 0, 0, 0, 0]
self.row5 = [0, 0, 0, 0, 0, 0, 0]
self.row6 = [0, 0, 0, 0, 0, 0, 0]
self.board_state_row = []
self.board_state_collumn = []
self.board_state_diagonal = []
def drop_stones(self):
l1 = [i for i, v in enumerate(self.row2) if v == 0]
for i in l1:
self.row2[i] = self.row1[i]
self.row1[i] = 0
l2 = [i for i, v in enumerate(self.row3) if v == 0]
for i in l2:
self.row3[i] = self.row2[i]
self.row2[i] = 0
l3 = [i for i, v in enumerate(self.row4) if v == 0]
for i in l3:
self.row4[i] = self.row3[i]
self.row3[i] = 0
l4 = [i for i, v in enumerate(self.row5) if v == 0]
for i in l4:
self.row5[i] = self.row4[i]
self.row4[i] = 0
l5 = [i for i, v in enumerate(self.row6) if v == 0]
for i in l5:
self.row6[i] = self.row5[i]
self.row5[i] = 0
def update(self):
# board horizontal
self.board_state_row = [self.row1, self.row2, self.row3, self.row4, self.row5, self.row6]
# board vertical
self.board_state_collumn = []
for i in range(0, 7, 1):
self.board_state_collumn.append(self.rows_to_collumns(i))
# board diagonal
rows = 6
collumns = 7
diagonal1 = defaultdict(list) # For the top right to bottom left
diagonal2 = defaultdict(list) # For the top left to bottom right
for i in range(rows):
for j in range(collumns):
diagonal1[i - j].append(self.board_state_row[i][j])
diagonal2[i + j].append(self.board_state_row[i][j])
self.board_state_diagonal = []
self.board_state_diagonal.insert(0, diagonal1)
self.board_state_diagonal.insert(1, diagonal2)
def make_turn(self, slot, activeplayer):
if activeplayer == 1:
self.symbol = 1
if activeplayer == 2:
self.symbol = -1
if slot in range(0, 7):
self.row1[slot] = self.symbol
return True
else:
return False
def print_board(self):
print(
' 1 ', ' | ',
' 2 ', ' | ',
' 3 ', ' | ',
' 4 ', ' | ',
' 5 ', ' | ',
' 6 ', ' | ',
' 7 ', ' | ',)
# row1
print(
self.state_to_sign(self.row1[0]), ' | ',
self.state_to_sign(self.row1[1]), ' | ',
self.state_to_sign(self.row1[2]), ' | ',
self.state_to_sign(self.row1[3]), ' | ',
self.state_to_sign(self.row1[4]), ' | ',
self.state_to_sign(self.row1[5]), ' | ',
self.state_to_sign(self.row1[6]), ' | ')
# row2
print(
self.state_to_sign(self.row2[0]), ' | ',
self.state_to_sign(self.row2[1]), ' | ',
self.state_to_sign(self.row2[2]), ' | ',
self.state_to_sign(self.row2[3]), ' | ',
self.state_to_sign(self.row2[4]), ' | ',
self.state_to_sign(self.row2[5]), ' | ',
self.state_to_sign(self.row2[6]), ' | ')
# row3
print(
self.state_to_sign(self.row3[0]), ' | ',
self.state_to_sign(self.row3[1]), ' | ',
self.state_to_sign(self.row3[2]), ' | ',
self.state_to_sign(self.row3[3]), ' | ',
self.state_to_sign(self.row3[4]), ' | ',
self.state_to_sign(self.row3[5]), ' | ',
self.state_to_sign(self.row3[6]), ' | ')
# row4
print(
self.state_to_sign(self.row4[0]), ' | ',
self.state_to_sign(self.row4[1]), ' | ',
self.state_to_sign(self.row4[2]), ' | ',
self.state_to_sign(self.row4[3]), ' | ',
self.state_to_sign(self.row4[4]), ' | ',
self.state_to_sign(self.row4[5]), ' | ',
self.state_to_sign(self.row4[6]), ' | ')
# row5
print(
self.state_to_sign(self.row5[0]), ' | ',
self.state_to_sign(self.row5[1]), ' | ',
self.state_to_sign(self.row5[2]), ' | ',
self.state_to_sign(self.row5[3]), ' | ',
self.state_to_sign(self.row5[4]), ' | ',
self.state_to_sign(self.row5[5]), ' | ',
self.state_to_sign(self.row5[6]), ' | ')
# row6
print(
self.state_to_sign(self.row6[0]), ' | ',
self.state_to_sign(self.row6[1]), ' | ',
self.state_to_sign(self.row6[2]), ' | ',
self.state_to_sign(self.row6[3]), ' | ',
self.state_to_sign(self.row6[4]), ' | ',
self.state_to_sign(self.row6[5]), ' | ',
self.state_to_sign(self.row6[6]), ' | ')
def rows_to_collumns(self, index):
collumn = []
for i in self.board_state_row:
collumn.append(i[index])
return collumn
def check_win_horizontal(self):
for i in range(0, 6, 1):
wincounter = 0
for j in self.board_state_row[i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
else:
wincounter = 0
def check_win_vertical(self):
for i in range(0, 7, 1):
wincounter = 0
for j in self.board_state_collumn[i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
def check_win_diagonal(self):
for i in range(-6, 5, 1):
wincounter = 0
for j in self.board_state_diagonal[0][i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
else:
wincounter = 0
for i in range(0, 11, 1):
wincounter = 0
for j in self.board_state_diagonal[1][i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
else:
wincounter = 0
@staticmethod
def state_to_sign(state):
if state == 1:
return colored(' @ ','red')
if state == -1:
return colored(' @ ','yellow')
if state == 0:
return ' '
class Player:
def __init__(self, player_number):
self.name = 'default'
self.player_number = player_number
def set_name(self, name):
self.name = name
def start_game():
player1 = Player(1)
player2 = Player(2)
board = Board()
player1.set_name(input('Whats your name Player1?: '))
player2.set_name(input('Whats your name Player2?: '))
activeplayer = 1
while activeplayer > 0:
try:
board.drop_stones()
while activeplayer == 1:
board.print_board()
slot = int(input('Which slot do you choose ' + player1.name + '? 1-7 or 0 to exit: '))
if slot == 0:
exit()
if slot > 0:
slot = slot - 1
if board.row1[slot] != 0:
print('Slot full! Pick another one 1-7: ')
break
board.make_turn(slot, activeplayer)
board.update()
board.drop_stones()
board.update()
if board.check_win_diagonal() or board.check_win_horizontal() or board.check_win_vertical():
board.print_board()
print('you win', player1.name + '!')
activeplayer = 0
if not board.check_win_diagonal() \
and not board.check_win_horizontal() \
and not board.check_win_vertical():
activeplayer = 2
else:
print('Out of range! 1-7: ')
break
while activeplayer == 2:
board.print_board()
slot = int(input('Which slot do you choose ' + player2.name + '? 1-7 or 0 to exit: '))
if slot == 0:
exit()
if slot > 0:
slot = slot - 1
if board.row1[slot] != 0:
print('Slot full! Pick another one 1-7: ')
break
board.make_turn(slot, activeplayer)
board.update()
board.drop_stones()
board.update()
if board.check_win_diagonal() or board.check_win_horizontal() or board.check_win_vertical():
board.print_board()
print('you win', player2.name + '!')
activeplayer = 0
if not board.check_win_diagonal() \
and not board.check_win_horizontal() \
and not board.check_win_vertical():
activeplayer = 1
else:
print('Out of range! 1-7: ')
break
except IndexError:
print('Out of range... 1-7! ')
except ValueError:
print('Only numbers! 1-7: ')
sleep(1.5)
start_game()
3 Answers 3
Cool program!
Computers are machines for automation; any time you find your code is repetitive, like in print_board
or the two players in start_game
, it's very likely you can make the code shorter by using an array or a function.
A few other odds and ends:
The
try
block instart_game
is quite long, and catchesIndexError
andValueError
, errors that very often indicate a bug. So if there is a bug in any of that code that causes an IndexError, the program would just print "Out of range... 1-7!
" and keep going. The error message would be lost.It's considered good style in Python to separate words in variable names with
_
, soactive_player
rather thanactiveplayer
.It's also considered good style for a method like
check_win_horizontal
toreturn False
at the end, so that it always returns eitherTrue
orFalse
. If you don't return anything at the end of a function, Python returnsNone
by default, so the program still works. But it's just a little confusing.Your program doesn't check for the possibility of a tie game. It's possible to fill up the entire grid without anyone winning.
The printed board seems to need a
|
and some more space on the left.The English word "columns" only has one L in it.
But all of this is very minor. It's a fine program. Hope you're having fun with Python! :)
There are a lot of issues with your code. I will try to cover all of them.
Logic Repetiton
- In your
start_game()
function, you are repeating the almost same loop for both players.
You can make a current_player
and use it instead of player1
or player2
.
- You can just use a
else
statement in this piece of code -
if board.check_win_diagonal() or board.check_win_horizontal() or
board.check_win_vertical():
...
if not board.check_win_diagonal() \
and not board.check_win_horizontal() \
and not board.check_win_vertical():
activeplayer = 1
Instead -
if board.check_win_diagonal() or board.check_win_horizontal() or
board.check_win_vertical():
...
else:
activeplayer = 1
Unnecessary objects
I would prefer to use a
grid
variable storing all the rows rather than making each row a seperate variable. Then you can iterate over each row with aloop
.Your class
Player
does not do much. You could just useplayer1
andplayer2
variables. Also you are never usingPlayer.player_number
.
Print board function
- You can make your function shorter by using a
for loop
.
def print_board(self):
print('| 1 | 2 | 3 | 4 | 5 | 6 | 7 |')
for row in self.grid:
print('| ', end='')
for index in range(7):
print(str(self.state_to_sign(row[index])) + '| ', end='')
print('')
Ignoring Performance
- Your
drop_stones()
function checks all columns. This affects the performance. Instead check only the column chosen by the user.
Other improvements(do not matter much)
- By default the step parameter of range is 1. You do not need to specify it.
range(1, 10)
instead ofrange(1, 10, 1)
.
Happy Coding!
Edit:
This is the review for the optimized code posted again by OP.
Coupling
"How easy is it to cut out a piece of code?" If the answer is "very difficult" then our system is likely tightly coupled. Ideally, it is easy to remove code (say we find it doesn't work as intended and need to replace it) and it is easy to run that cut out code by itself (say we want to test it).
Most of the functions look good. There are some that could be improved.
def check_tie(self):
if 0 not in self.board_state_row[0]:
print('You tied! No one Wins!')
sleep(1.5)
exit()
What if you wanted to test this function?(I know its very simple function)
You wont be able to check it multiple times because it will end the program.
You want to only return if its a tie or not and do the necessary outside the function.
def check_tie(self):
return 0 not in self.board_state_row[0]
Credit for the explanation to spyr03
List Comprehension
- When ever you can, you should use a list comprehension as it almost always faster than for loops.
So you can upgrade your
columns_to_rows()
accordingly.
if name == 'main'
- You should always use
if __name__ == '__main__'
in your files.
You should wrap your all all your while active_player > 0
code in a function play()
and then use it under the if __name__ == '__main__'
clause
if __name__ == '__main__':
play()
You can read more about it on this Stack Overflow answer.
F Strings
- You can use
f-strings
fromPython 3.6
.f-strings
are simply faster than%s
andstr.format()
.
Other Improvement
- In this piece of code:
if active_player == 1:
return int(2)
if active_player == 2:
return int(1)
You can just return active_player
.
- When defining matrices, it is nice to define it like this:
matrix = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
It also is easier to understand its structure.
- You can use the
+=
operator to perform some of your calculations.
Example - a = a + 1
is same as a += 1
.
-
\$\begingroup\$ Thank you very much, i will look into everything you mentioned and try to improve things :) \$\endgroup\$Alexander Müller– Alexander Müller2021年09月21日 18:33:34 +00:00Commented Sep 21, 2021 at 18:33
So finally, with your super helpful tips and after a few hours of improving stuff I got rid of a few lines of code, all the self.row
variables and improved some of the functions. Also I got rid of the Player
class since it was only storing the names, which I implemented as variables now.
The drop_stones
and print_board
methods are also much less code now and drop_stones
now uses the selected column instead of every row so it has to check and change only one list instead of a whole 2d list. Next thing on the list would be a function to check for a tie, but I'm sure I need another few hours to come up with a crappy solution for that XD
Edit: I wrote the check_tie function to check for a tie (when the first of the row is full and no one has won). Also I fixed a bug where input was declared as str
while int
was needed.
Here is the "optimized" code:
from collections import defaultdict, deque
from termcolor import colored
from time import sleep
class Board:
def __init__(self):
self.symbol = 0
self.board_state_row = [[0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0], [0,0,0,0,0,0,0]]
self.board_state_column = [[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]]
self.board_state_diagonal = []
def drop_stones(self, slot):
a = self.board_state_column[slot]
b = deque([x for x in a if x != 0])
for i in a:
if i == 0:
b.appendleft(0)
self.board_state_column[slot] = b
def update(self):
# board rows
self.board_state_row = []
for i in range(6):
self.board_state_row.append(self.columns_to_rows(i))
# board diagonals
rows = 6
columns = 7
diagonal1 = defaultdict(list) # For the top right to bottom left
diagonal2 = defaultdict(list) # For the top left to bottom right
for i in range(rows):
for j in range(columns):
diagonal1[i - j].append(self.board_state_row[i][j])
diagonal2[i + j].append(self.board_state_row[i][j])
self.board_state_diagonal = []
self.board_state_diagonal.insert(0, diagonal1)
self.board_state_diagonal.insert(1, diagonal2)
def make_turn(self, slot, active_player):
if active_player == 1:
self.symbol = 1
if active_player == 2:
self.symbol = -1
if slot in range(0, 7):
self.board_state_column[slot][0] = self.symbol
return True
else:
return False
def print_board(self):
for i in range(1,8):
print(' '+str(i)+' ', ' | ', end= ' ')
print('')
for i in range(6):
for j in range(7):
print(self.state_to_sign(self.board_state_row[i][j]), ' | ',end=' ')
print('')
def columns_to_rows(self, index):
row = []
for i in self.board_state_column:
row.append(i[index])
return row
def check_win_horizontal(self):
for i in range(6):
wincounter = 0
for j in self.board_state_row[i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
else:
wincounter = 0
if wincounter == 0:
return False
def check_win_vertical(self):
for i in range(7):
wincounter = 0
for j in self.board_state_column[i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
if wincounter == 0:
return False
def check_win_diagonal(self):
for i in range(-6, 5):
wincounter = 0
for j in self.board_state_diagonal[0][i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
else:
wincounter = 0
for i in range(11):
wincounter = 0
for j in self.board_state_diagonal[1][i]:
if j != self.symbol:
wincounter = 0
if j == self.symbol or wincounter == 0:
if j == self.symbol:
wincounter = wincounter + 1
if wincounter == 4:
return True
else:
wincounter = 0
if wincounter == 0:
return False
def check_tie(self):
if 0 not in self.board_state_row[0]:
print('You tied! No one Wins!')
sleep(1.5)
exit()
@staticmethod
def state_to_sign(state):
if state == 1:
return colored(' @ ','red')
if state == -1:
return colored(' @ ','yellow')
if state == 0:
return ' '
def game(current_player,active_player):
board.print_board()
slot = int(input('Which slot do you choose ' + current_player + '? 1-7 or 0 to exit: '))
if slot == 0:
exit('Exit by selection.')
if slot > 0:
slot = slot - 1
while board.board_state_row[0][slot] != 0:
slot = int(input('Slot full! Pick another one 1-7: '))
slot = slot -1
board.make_turn(slot, active_player)
board.update()
board.drop_stones(slot)
board.update()
if board.check_win_diagonal() or board.check_win_horizontal() or board.check_win_vertical():
board.print_board()
print('you win '+ current_player + '!')
sleep(1.5)
exit(0)
board.check_tie()
if active_player == 1:
return int(2)
if active_player == 2:
return int(1)
board = Board()
player1 = input('Whats your name Player1?: ')
player2 = input('Whats your name Player2?: ')
active_player = 1
while active_player > 0:
try:
while active_player == 1:
active_player = game(player1, active_player)
while active_player == 2:
active_player = game(player2, active_player)
except IndexError:
print('Out of range... 1-7! ')
except ValueError:
print('Only numbers! 1-7: ')
sleep(1.5)
-
\$\begingroup\$ I edited my answer to higlight some issues in your code. Have a look. \$\endgroup\$Random_Pythoneer59– Random_Pythoneer592021年09月23日 06:48:43 +00:00Commented Sep 23, 2021 at 6:48
Explore related questions
See similar questions with these tags.