Welcome to part 12 of the Python 3 basics series, where we're going to be talking about how we can validate the winners of our TicTacToe game in the final way: diagonally!
As we've done before, let's just start clean with a game example of a diagonal win:
game = [[1, 0, 1], [1, 1, 2], [2, 2, 1]]
Now, how might we validate the diagonal wins? We know we could hard-code it with:
if game[0][0] == game[1][1] == game[2][2] and game[0][0] != 0: print("winner!") elif game[2][0] == game[1][1] == game[0][2] and game[2][0] != 0: print("winner!")
Obviously, this doesn't scale either, but this is the sort of logic we need. Can we think of any sort of pattern that we could be able to just code in here?
Well, we can clearly see the indexes. One set of indexes is like [0][0]
, [1][1]
, [2][2]
, so just incrementing up for the size of the game and equal numbers. The other version is swapped numbers. One starts at 0 and goes up, the other starts at 2 (our max index) and goes down. This is very logical, and we can definitely code that. How might we do it? Let's start with the easiest one, where they are the same value. So far, we've continued the method of grabbing all the values we care about, then running the check if they're all the same, so we'll just do that again:
game = [[1, 0, 1], [1, 1, 2], [2, 2, 1]] diags = [] for ix in range(len(game)): diags.append(game[ix][ix]) print(diags)
So here, we just grab the index value by simply iterating over the range
of the length of the game (assuming here the game is square, as it should be).
The output is:
[1, 1, 1] >>>
Great, we did it! We can of course add in:
if diags.count(diags[0]) == len(diags) and diags[0] != 0: print("Winner!")
And now we know it's a winner! Awesome! What about the other way? We need some way to iterate...backwards over the range of the len. HmmMmMmmMM... gosh I wonder if there's a... built-in function for this???? Oh yes there is: reversed()
. Can we just do a reversed(range(len()))
then?
for i in reversed(range(len(game))): print(i)
Sure can!
2 1 0
With this, how can we collect the combo of [0][2]
, [1][1]
, [2][0]
? Well, we might try:
cols = reversed(range(len(game))) rows = range(len(game))
Then maybe something like:
for idx in rows: print(idx, cols[idx])
Unfortunately, we see:
Traceback (most recent call last): File "C:\Users\H\Desktop\python3-updated-series\part12.py", line 11, inprint(idx, cols[idx]) TypeError: 'range_iterator' object is not subscriptable >>>
Darnit! Let's just quit this Python stuff, it's too hard. How are we supposed to know what the heck that error means?! Oh right, the secret dev tool called google.com!
We can just search this exact error: TypeError: 'range_iterator' object is not subscriptable
. Top result says: The reversed function returns an iterator, not a sequence.
. Then suggests some stuff we probably don't fully understand at this point.
Second result though... (hey, noticing a trend???) says: You need to change r back to a list type. For example:
Now that's something we can understand easily, and, what's that? ...another built-in function called list()
! Dang these things are handy. Also. What if we didn't have list()
though? We could build the list by iterating over the reversed range and using append just fine. But, we've found this, so let's do that. We just need to convert it to a list, like so:
cols = list(reversed(range(len(game)))) rows = range(len(game))
game = [[1, 0, 1], [0, 1, 2], [1, 2, 1]] cols = list(reversed(range(len(game)))) rows = range(len(game)) for idx in rows: print(idx, cols[idx])
0 2 1 1 2 0 >>>
But wait, there's *moar*!!!
As if you haven't had your fill of built-in functions, I've got another one for you, because this whole thing:
for idx in rows: print(idx, cols[idx])
Isn't quite the easiest to read. Also, this is a super common task that programmers need to do (iterate over 2 lists together). In programming, there's a term called DRY (Don't Repeat Yourself), which also tends to extend to "dont repeat others" too, which is where Python's built-in functions come in. So, combining two lists? Super simple. It's done using the zip() function!
Wow, that looks like fun, let's try that instead.
for col,row in zip(cols,rows): print(col, row)2 0 1 1 0 2 >>>
Personally, I think that's a LOT simpler to read.
So I would probably stop here in practice, but sometimes it's fun to try to condense code. I do get a certain sense of enjoyment when I write something exceptionally brief, but it does a lot. It's a good learning experience, but this doesn't mean you should use it in your code if it makes your code harder to read. So let's just use this as a learning experience.
First off, we can easily remove 2 rows of code that define the cols and rows:
cols = list(reversed(range(len(game)))) rows = range(len(game)) for col, row in zip(cols, rows): print(col, row)
...because we can just put those in the zip statement like this:
for col, row in zip(reversed(range(len(game))), range(len(game))): print(col, row)
2 0 1 1 0 2 >>>
Like I said, not easier to read...but hey, we got rid of 2 lines.
...but wait. We...could actually condense this even further. For example, what are we actually doing here:
range(len(game))
...we're just counting up from 0, right? We're just getting the index. Isn't there... A BUILT IN FUNCTION FOR THAT??!?!?! woo enumerate
!
for idx, reverse_idx in enumerate(reversed(range(len(game)))): print(idx, reverse_idx)
0 2 1 1 2 0 >>>
Hah. Okay that was fun. There are some further ways we could condense this...but let's move on I guess, because we're almost done! Okay, so, for the reversed diagonal, we can do:
game = [[0, 0, 1], [0, 1, 2], [1, 2, 1]] diags = [] for idx, reverse_idx in enumerate(reversed(range(len(game)))): diags.append(game[idx][reverse_idx]) if diags.count(diags[0]) == len(diags) and diags[0] != 0: print("Winner!")
Okay, we've got all the bases covered! Now, let's combine it all!
game = [[0, 0, 1], [0, 1, 2], [1, 2, 1]] def win(current_game): # horizontal for row in game: print(row) if row.count(row[0]) == len(row) and row[0] != 0: print(f"Player {row[0]} is the winner horizontally!") # vertical for col in range(len(game[0])): check = [] for row in game: check.append(row[col]) if check.count(check[0]) == len(check) and check[0] != 0: print(f"Player {check[0]} is the winner vertically!") # / diagonal diags = [] for idx, reverse_idx in enumerate(reversed(range(len(game)))): diags.append(game[idx][reverse_idx]) if diags.count(diags[0]) == len(diags) and diags[0] != 0: print(f"Player {diags[0]} has won Diagonally (/)") # \ diagonal diags = [] for ix in range(len(game)): diags.append(game[ix][ix]) if diags.count(diags[0]) == len(diags) and diags[0] != 0: print(f"Player {diags[0]} has won Diagonally (\\)") win(game)
Output here:
[0, 0, 1] [0, 1, 2] [1, 2, 1] Player 1 has won Diagonally (/) >>>
Let's check a few more variations:
game = [[0, 2, 1], [0, 2, 2], [1, 2, 1]]
[0, 2, 1] [0, 2, 2] [1, 2, 1] Player 2 is the winner vertically! >>>
[0, 0, 1] [0, 0, 2] [1, 0, 1] >>>
[0, 0, 0] [0, 0, 2] [0, 0, 1] >>>
[1, 0, 0] [0, 1, 2] [0, 0, 1] Player 1 has won Diagonally (\) >>>
[2, 2, 2] Player 2 is the winner horizontally! [0, 1, 2] [0, 0, 1] >>>
Okay, good enough I'd say. Next, we have some cleaning to do, I see a lot of repetition with our checking statement (4x?! horrible!). We also have 1 remaining non-dynamic bit (the printed 0, 1, 2 for the column #s) that we absolutely must fix. Beyond this, we just need to bring our win function back to the rest of our game, accept user input, create a loop to play til there is a winner, and we're good to go!