Closed alexmojaki closed 3 years ago
There might also be some good ideas here when I get around to reading it: https://www.daniellowengrub.com/blog/2020/02/08/python-for-beginners
Hey @alexmojaki , I'm writing a Markdown draft for this. I have a question.
diagonal_winner
does NOT check if piece
is None
or not, but row_winner
and column_winner
do.
Then inside winner
there is None
checking for all three functions, necessarily.
The implication is that if the return
statements are not triggered, Python will make them return None
. I can certainly explain this if it wasn't covered before.
Since we have to check all three inside winner
anyway, it feels like making all three consistent with each other would be better.
What do you think? Should I make all three check for None
, or make none of them check, or leave it as it is?
I would prefer leaving all the checks to winner
because the others are redundant and this would make them simpler (as long as the user is aware when those functions return None
).
I think I have an easier way to write these functions. Don't write a draft yet.
In any case we still need to cover these concepts first at a minimum:
I think I have an easier way to write these functions.
OK, so let me explain. In the combining booleans chapter, diagonal_winner only has to return whether or not a winner is present, which is easier to write. And that's all we really need, because if there's a winner, it's obviously whoever just played. So for example they could write:
def row_winner(board):
result = False
for row in board:
piece = row[0]
if piece != ' ' and piece == row[1] and piece == row[2]:
result = True
return result
def winner(board):
# Unlike before, this becomes the only sensible way
return row_winner(board) or column_winner(board) or diagonal_winner(board)
def main():
...
player = 'X'
for _ in range(9):
...
if winner(board):
print(player + ' wins!')
I think doing this to make the project easier is a good thing.
Regarding your question: the fewer concepts they are required to understand, the better. So let's avoid implicitly returning None or the falsiness of None. With that, I don't think there's much reason to use None at all, so maybe the board should start out filled with spaces instead of None, which also makes it easy to write format_board. On a similar note, let's not mention enumerate or zip.
Also, since we have established unit tests as part of the course, it should be format_board, not print_board. But that means we need to teach newlines, so maybe that should go in the 'more about strings' chapter about quotes and f-strings.
Nice! I like those changes. I'll get to it after my current assignment.
Ok, I looked through the draft in #92. Let's keep that level of discussion here, I was only taking about it there as it related to other pages that will be in the course.
Unfortunately it seems like you forgot about my comment above, where I said the functions should return booleans, not strings. They only have to detect the presence of a winner, not who won. That drastically changes what you wrote, so I haven't looked at the whole thing in detail.
Also dumping some other thoughts:
Maybe one last part of the project can be iterable unpacking. At the end of the current design there will be some code like this:
coordinates = get_free_coordinates(board)
row = coordinates[0]
column = coordinates[1]
Ultimately we want
row, column = get_free_coordinates(board)
On second thought, this is pushing things a bit. Maybe we should just simplify that whole part to:
def play_move(board, player):
while True:
row = get_coordinate('row')
col = get_coordinate('column')
if board[row][col]:
print("That spot is taken")
else:
board[row][col] = player
return
format_board needs assertions with strings containing newlines. How do we do that in a student friendly way? The simplest code to understand is:
assert_equal(
format_board([
['X', 'O', 'X'],
['O', ' ', ' '],
[' ', 'X', 'O']
]),
"XOX\nO \n XO"
)
but it completely hides why we format the board that way.
Do we split the string into several?
assert_equal(
format_board([
['X', 'O', 'X'],
['O', ' ', ' '],
[' ', 'X', 'O']
]),
"XOX\n" +
"O \n" +
" XO"
)
That looks pretty ugly, and is screaming for a triple quoted string. But then we have another thing to teach, and the simplest version of that is ugly too:
assert_equal(
format_board([
['X', 'O', 'X'],
['O', ' ', ' '],
[' ', 'X', 'O']
]),
"""XOX
O
XO""")
To make it better requires more concepts:
assert_equal(
format_board([
['X', 'O', 'X'],
['O', ' ', ' '],
[' ', 'X', 'O']
]),
"""\
XOX
O
XO""")
assert_equal(
format_board([
['X', 'O', 'X'],
['O', ' ', ' '],
[' ', 'X', 'O']
]),
"""
XOX
O
XO
""".strip())
Also I'm assuming that the result doesn't end in a newline, but that makes things a bit harder, maybe it should be allowed?
Finally I think the initial format_board should have no numbers on the side. Adding numbers can be an extra exercise, maybe it should even be a bonus challenge.
Haha yeah sorry! The discussion is split all over the place so it seems I forgot. I think I'll just edit/update my draft to return booleans.
I prefer your second thought. (I think we may consider teaching tuples maybe somewhere else, then change this later to reflect it, like we did with f-strings.)
I think it's best to stick to the simplest "XOX\nO \n XO"
first. I think it's important for them to deal with a single-line string with a bunch of \n
s in it. They will encounter this a lot in the wild. Once the user gets a working playable version of the game, we can suggest one of the other options as an improvement and further polish.
I guess I should post the draft here?
(Outline at the beginning can be changed, I can remove the row-column numbers 123
and the names of the functions can be changed/added... I'm not too clear on the function names in the final build)
Tic-tac-toe Page 1, second draft (returning booleans this time)
Don't feel bad for forgetting that comment, no one is perfect.
I'm thinking now that the next_player
function is probably micromanaging and overkill. They can structure the main
function however they'd like, making more smaller functions if they want. That's a skill they need to practice.
This solution might loop through board twice (once for each if statement)
There are no if statements now.
Keep in mind that some entries might be ' '. An empty row is not a winning row.
Sounds like a message step.
The row_winner solution shouldn't be __copyable__
. We want the user to study the solution and compare it to their own, maybe editing their solution.
We will need to teach about ending a function with an early return in the functions chapter.
You can even write the chain as if ' ' != piece == row[1] == row[2]:
. I don't know why you say "We cannot do the same with or" since there's no or
in the function.
I think it might be better if we just go straight to the general version. It saves time by getting straight to the point, especially if the user is tempted to solve the initial version with something like board[0][0] == board[0][1] == board[0][2] or board[1][0] == ...
. I also think that this size of problem is a good match with their current skills in breaking down and solving problems, i.e. doable but still challenging. If they're not ready, a hint can suggest thinking about the 3x3 version first. Meanwhile the project is showing them how to break down an even larger problem. What do you think?
Maybe rather than saying they can't use literals 'X' or 'O', we should require that the solution works for any characters as pieces, except ' ' of course.
All of these functions should have tests using assert_equal
.
It's not always necessary to show the user a solution after they complete an exercise. After all, they already have a solution. I think showing the generalised row_winner is not very useful, since you don't say anything about it.
Hmm OK... some of those irrelevant texts are left over from the previous version, my bad. Even though I've read through it twice after changing it to return booleans, I still missed them.
I wasn't sure what I thought about the difficulty. First I thought the difficulty level was too high. But hey, I'm all for getting rid of the 3x3 versions if you think they are capable! I think I need to redo the course every now and then to "calibrate" my sense of where the user would be skill-wise. It's true the previous chapters had some tough stuff. They should be able to handle it; plus there are tons of hints.
In terms of breaking down, I was thinking of maybe having them write separate functions to check an individual row/column/diagonal, then use that in the bigger function? This is probably not a good idea; first of all as you said we want them to practice this skill on their own; second the functions are not that long anyway.
Right, assert_equal
tests come with the text. Again I should re-read the latest chapters.
I'd like to make one more version of the draft before moving forward.
Page 1, third draft: removed 3x3 versions of functions, made suggested changes, added assert_equal
tests... hopefully I haven't forgotten something.
Change the text near the beginning to:
As in the last chapter, we will represent the tic-tac-toe board as a nested list of strings. For a typical game this will be a 3x3 list, i.e. 3 lists each containing 3 strings, with players represented by 'X'
or 'O'
. Empty squares will be represented by a space, i.e. ' '
. For example:
board = ...
However to make things more interesting your code will need to work for square boards of any size (4x4, 5x5, etc) where players can be represented by any two strings, e.g.
board = 4x4 using A and B
After that there's no need to mention the generalisation, except briefly to contrast diagonal_winner with the original version. Have one test in row_winner with pieces A and B just to make the point clear, but otherwise stick to X and O which are easier to read. Also keep them 3x3 and 4x4 and not too crowded. I say all this because for example this test is not easy to read:
assert_equal(
diagonal_winner(
[
['M', ' ', 'S', ' ', 'S'],
['S', 'M', 'S', 'S', ' '],
[' ', 'S', 'S', ' ', ' '],
['M', 'S', 'S', 'M', 'M'],
['S', 'S', 'S', 'S', 'S']
]
),
True
)
The tests not shown to the user should still have general sizes and pieces, including those made by generate_inputs.
should I add break here?
I think yes, to make the parsons problem more interesting.
I think it's best to stick to the simplest "XOX\nO \n XO" first. I think it's important for them to deal with a single-line string with a bunch of \ns in it. They will encounter this a lot in the wild.
You're right, it's worth feeling comfortable with this.
Once the user gets a working playable version of the game, we can suggest one of the other options as an improvement and further polish.
Well no, this is entirely about how the tests are written.
In terms of breaking down, I was thinking of maybe having them write separate functions to check an individual row/column/diagonal, then use that in the bigger function? This is probably not a good idea; first of all as you said we want them to practice this skill on their own; second the functions are not that long anyway.
I also wondered about this, a sort of all_equal
function. I think you're right that we shouldn't need it.
I think this draft of page 1 is good enough for a PR. Do you want to make a PR for just the start of the project? There's a slight chance that writing/drafting subsequent pages will reveal some broader structural problem with the first page, but I think it'll be OK.
OK, I predicted you'd ask me to shorten those tests.
Probably it will be easier on both of us if I make one PR per page. I thought about it too, we'll probably have to go back and change some things in earlier pages once the later pages are done. Let's not worry about it.
Going for the PR now...
Wasn't sure if I should put this here or #92
I decided to be proactive and started writing the PR. Ran into a bit of an issue with predicted output choices (of course if you don't like the exercise we can change it too):
This is line 1
This is line 2
This is line 3
and
This is line 1
This is line 2
This is line 3
are treated as the same. The radio button selects both of them when clicked. They both count as the correct choice.
I'm guessing this is a feature not a bug... it probably removes empty lines for a good reason. There might be a way of tricking it into accepting the empty line but I don't know. I tried hitting Enter, and tried adding \n. I should probably read the code :)
Yes, futurecoder strips whitespace in lots of places, especially trailing whitespace. I think that's for the best, rendering whitespace is often difficult (Github isn't showing the difference in your draft) and students shouldn't have to look for a trailing newline. The difference between options should be obvious.
I'm realising now how tricky this is. I will write a draft explaining newlines in the first part. In fact I will even start with triple quoted strings.
Interesting... I suppose that will change many things. For now I'll sit on it and do nothing, wait for your draft.
OK here's a draft
Next we want to tackle the problem of displaying the tic-tac-toe board. Here's one way to do this:
__copyable__
def print_board(board):
for row in board:
print("".join(row))
print_board([
['X', 'O', 'X'],
[' ', 'O', 'O'],
[' ', 'X', ' ']
])
(What's "".join
? Google it!)
[I know that in the past I've always made sure to introduce any new concept slowly and carefully, but I realise now that this hand-holding has to stop at some point so that students can learn to be independent and deal with seeing new and unfamiliar code on their own. Maybe this should have even been sooner.]
[Student runs code]
This is a good start but ideally we'd like a function which returns a string rather than printing it. This way other code can make easy use of the string in different ways. We might want to manipulate the string (e.g. draw a box around it or extract only the first few lines), we might want to send it somewhere other than the screen (e.g. a file) and in this particular case we want to be able to test it with assert_equal
. This doesn't work:
assert_equal(print_board([...]), "...")
because print_board
doesn't use return
so it just returns None
by default.
[The following section is overkill and could easily bore students. It should likely be excluded entirely. Maybe we could make it collapsible so that it's clearly optional?]
It's possible (though inconvenient) to get what print_board
prints as a string. But this is also an example of some more general good practices in programming:
board
pretty for humans to look at' and 'print the board on the screen' are two different things that can change independently. You might later decide to change how the board looks or you might decide to put the board somewhere other than the screen.The print_board
kind of strategy is easy and is sometimes good enough. But we'd like you to learn good habits that will serve you well in the long term.
[End of overkill section]
So instead we want code like this:
def format_board(board):
...
return ...
assert_equal(format_board([...]), "...")
Then print(format_board(board))
should print something like what we saw at the beginning.
But how do we return a string with multiple lines? And how do we test it? We'd like to do something like this:
assert_equal(
format_board([
['X', 'O', 'X'],
[' ', 'O', 'O'],
[' ', 'X', ' ']
]),
"XOX
OO
X "
)
See for yourself how this doesn't work.
[Student runs code, we check for syntax error]
Normally a string literal has to be on one line, so this is invalid:
string = "First line
Second line"
print(string)
But Python provides a way! The solution is to use triple quotes, i.e. three quote characters in a row (either '''
or """
) around the contents of the string:
string = """First line
Second line"""
print(string)
[Student runs code]
Hooray! A triple quoted string is allowed to span many lines and they will be shown in the output.
[Another collapsible section?]
The string literal is a bit ugly and doesn't really match the output. It would be nice if we could write:
print("""First line
Second line""")
but any spaces inside quotes (even in an indented block such as for
or def
) are actual characters in the string which will be printed, see for yourself.
Alternatively you could write:
print("""
First line
Second line""")
but this actually prints three lines, the first one being blank.
[End of section]
Like single and double quotes, triple quotes are just another kind of notation, not a new kind of string. """abc"""
is the same thing as "abc"
.
However string
does contain something new. Run string
in the shell to see.
[Student runs code. Must have a message to check that string
has the right value, see word_must_be_hello
in chapter 3]
There's the secret!
\n
represents a newline character. This is just another character, like a letter or a space (' '
). It's the character between two separate lines that you type in by pressing Enter on your keyboard.
Again, \n
represents the newline character within a Python string literal. The string doesn't actually contain \
and n
, it just contains one character. Check this in the shell:
len('\n')
[Student runs code, must predict output 1 or 2 ]
Now use the newline character to write the function format_board
:
def format_board(board):
...
assert_equal(
format_board([
['X', 'O', 'X'],
['O', ' ', ' '],
[' ', 'X', 'O']
]),
'XOX\nO \n XO'
)
Because we've revealed join
I'd like to hope that they use it, which is why I've removed the trailing newline. The official solution shouldn't use it but after they solve the exercise we can show a solution using join
twice and leave it up to them to figure out how it works.
Maybe the 'collapsible sections' could be placed in the final_text as sort of afterthoughts?
Nice work.
I'm OK with join
being mentioned. We should allow it in the format_board
solution as you said. The only issue is that the non-join solution will get a bit more complicated to exclude the final newline, but this is a pattern they've seen before many times (use a boolean and a loop, change boolean at some point...)
I agree the overkill section should probably be excluded entirely. I guess collapsible will be a new front-end feature? Can't remember seeing collapsible sections before. (We could go back and use it on the or section with the "Now I have additional questions" elephant picture, and maybe some others)
Yay for triple quotes! I wanted it originally.
The approach to "discover" the newline character is interesting. I guess they should be able to immediately use it. I hope they can immediately make the connection "Oh I can just +=
a newline just like any other string!".
Sure, we can include the collapsible parts in the final text as afterthoughts. We have to keep in mind that the final text comes after the bonus challenge, so the afterthoughts would be skippable that way. So maybe keep it collapsible before the bonus challenge?
Nice work.
I'm OK with
join
being mentioned. We should allow it in theformat_board
solution as you said. The only issue is that the non-join solution will get a bit more complicated to exclude the final newline, but this is a pattern they've seen before many times (use a boolean and a loop, change boolean at some point...)
Except remember we actually discovered it's tricky to do with just a boolean and made it a bonus challenge. It can be made easier now by looping over the indices and checking if we're on the first/last index.
Also it's kinda nice that the non-join solution is longer because it means more work if they reveal either kind of solution.
I agree the overkill section should probably be excluded entirely. I guess collapsible will be a new front-end feature?
Yes. Anyway it's more of a future idea.
We could go back and use it on the or section with the "Now I have additional questions" elephant picture, and maybe some others
Definitely.
Yay for triple quotes! I wanted it originally.
Yeah, sorry about all the debate and the wasted draft. It's weird how complicated I've managed to make teaching one little concept.
The approach to "discover" the newline character is interesting. I guess they should be able to immediately use it. I hope they can immediately make the connection "Oh I can just
+=
a newline just like any other string!".
I hope so too, I've given them plenty of opportunities to see that it's just a character. We can add a hint to make sure.
Sure, we can include the collapsible parts in the final text as afterthoughts. We have to keep in mind that the final text comes after the bonus challenge, so the afterthoughts would be skippable that way. So maybe keep it collapsible before the bonus challenge?
Ooooh, I forgot about that. Let's just leave them out for now.
OK, I guess I should make an updated draft with these changes? Or go straight to PR? Going for PR...
Last part finished in #162
Currently the course only contains isolated bite-sized exercises. Eventually students should work on a larger project. This is one idea for that.
Below is a simple implementation of a game to be played by 2 humans. It's broken up into several small functions which is helpful for many reasons. It could probably be broken up even further. For some functions I've provided several implementations at different levels using different concepts.
This project could also be taken to another level where the user has to implement automatic computer opponents, at least one which is completely random and one which is random except when there is an opportunity to win immediately. The two computer players plus the human player could be implemented as 3 classes in a hierarchy or as a case for higher order functions.
These are the minimal concepts that users will need:
Here's the code: