Creating a snake game in the terminal, from scratch
Reviewing your question - I didn’t really answer you need - it’s a college exercise reconstructing the steps of the covering from the direction - and it’s s[o theorist, with m 0 concern whether the game would be real or not.
Later on, maybe,:
Full Cover Game Tutorial in text mode
Good - you have another problem there -
if the game needs to be in text mode, there is no standardized way between operating systems like Linux and OS X and Windows on how to draw an interactive screen on the terminal - or read the keyboard in real time (without the player having to press <enter>
after each movement).
That is - this is a game that was once simple at a time when text interfaces offered a fixed control option. Now - just these functions of "see what character is in such position" and "print a character in such position" are what let create games like this in a simple way. I’m going to suggest a library to make such a little game in text mode - and then with a few examples, you’ll see that the game is at once simpler than what you’re doing, and more fun in terms of programming.
Nowadays, in the Linux and OS X environments, with Python, you can use the "curses" library that comes with Python. Windows, by default, does not work "curses" and you will have to use the library msvcrt
. You could determine that you will only need 3 things: (1) read one of the arrow keys in "real time" - (that is, without waiting for enter), (2) be able to clear the screen and (3) be able to print a character in an arbitrary position (row and column) of the screen. Hmmm, seeing the documentation -- "msvcrt" does not allow (3) - you are back in the bush without dog.
Ah - (santo Google) - there is a way to install "curses" for Windows - is in this accepted answer of stackoverflow in English:
https://stackoverflow.com/questions/32417379/what-is-needed-for-curses-in-python-3-4-on-windows7
That being said, Curses is a bit boring - your program either prints and uses input, as you do, or does everything for curses - its documentation is here: https://docs.python.org/3/library/curses.html - so I’m not even going to try to put up with this bunch of questions and answers to start the game you did. (and let’s face it - it’s pretty cool now that you already know about the input
and put its value in a variable - but who wants to answer a lot of questions just to start the game of the hedge? :-) )
I am consulting the official tutorial of curses of the Python site at https://docs.python.org/3/howto/curses.html#curses-howto and adapting to your question.
Let’s go in parts - first, let’s put a character in the text terminal that can be moved with the arrow keys, using the "curses" library. Unicode has a lot of cool characters, but since it’s supposed to be "root", let’s use a "*".
import curses
import locale
import time
locale.setlocale(locale.LC_ALL, '')
def main(screen):
curses.curs_set(0)
screen.nodelay(True)
y, x = 20, 20
direction = (0, 1)
while True:
key = screen.getch()
if key == curses.KEY_LEFT:
x -= 1
elif key == curses.KEY_RIGHT:
x += 1
elif key == curses.KEY_DOWN:
y += 1
elif key == curses.KEY_UP:
y -= 1
elif key in (0x1b, ord("q")): # <ESC>
break
screen.addstr(y, x, b"*")
screen.refresh()
time.sleep(0.1)
curses.wrapper(main)
You notice that the curses have a lot of bureaucracies - we have to call the setlocale
in order to be able to print Unicode characters (in a terminal with Unicode, of course), we have to call curs_set, nodelay to delete the cursor from the text mode and not to wait for the <enter>
, as a input
common. In addition, the call wrapper
calling the function main
already initializes the screen, and configures some 4 or 5 things.
With those calls out of the way, we can start thinking about the program -
I initially put "*" at the "20, 20" position (why ask the user that? - the game may have some pre-configured start positions, or it may simply draw a position, but it will not change the game by asking the initial position of the cover for the user - except for the worst).
In the body of this "while True" I establish what I call the game’s "main loop" - is the code Techo that runs the entire game frame - and that will update the position of the elements, read the keyboard, etc -- however sophisticated a game is - can be the GTA VIII - , it will have to have a "main loop" - in some game programming frameworks, the loop can be embedded in the framework, and you only configure some of its functions that will be called when events happen - but the loop is there.
Inside the loop, the first thing is to call screen.getch()
- is a curses method (actually in the "screen" object that wrapper passes to our function) that reads the numeric code of a single key. How do we define the nodelay
above, this call returns instantly - if no key is pressed, it will have a special value.
Then several lines of pure Python programming: 5 if
s comparing the code pressed with those of the arrow keys - as these values, are not standardized in ASCII or other encoding - unlike the "q" key or even the "ESC", we have to compare with preset constants in the curses module itself. (Other frameworks for games that allow keyboard reading also have to use this feature to have arrow code, function keys (F1, F2, ...), "prtscr", etc...)
But - so, it’s no secret if the user pressed "right arrow", we added 1 on the x coordinate, and so on.
Then the call to function addstr(linha, coluna, caractere)
puts the character in the set position. Ready - everything we needed! Only not - reading the doc, we see that it just draws even when we call the method refresh
- and finally, a 1/10 second scheduled pause - so that the asterisk does not move as quickly as possible. The duration of this break is that you will then control, according to the difficulty level, to determine the speed of the cover.
One last thing about the "bureaucracy" of using curses: everything you send to the screen, and take it back is in "bytes" - in Python 3, we work directly with "text" (the Python 3 strings are the old Python 2 "Unicode" object) - which means that curses know nothing of accented characters, or emojis - you have to pass everything chewed to it - normal Python strings have to be transformed into Bytes with your "Encode" method - or, in this case, as we are only putting simple characters, we can write the direct bytes by placing a b"
to open the string in the program code.
great - test so far - and keep in mind that when they created the first variants of the copper, in the 70’s, it was that terminal environment that people had.
Now, to begin to turn this into the game of the little snake: (1) it cannot stand still, it must always have a direction. If you use a single number for the direction as in your code, every time you use the direction you will have to use an "if" sequence to update anything. direcao = 3
: y coordinate should be updated? Up or down?
Since we will always have to update the x and y coordinates, the direction can be a sequence of two numbers, already with the values to update each variable.
import curses
import locale
import time
locale.setlocale(locale.LC_ALL, '')
def main(screen):
curses.curs_set(0)
screen.nodelay(True)
y, x = 20, 20
direction = (1, 0)
while True:
key = screen.getch()
if key == curses.KEY_LEFT:
direction = (-1, 0)
elif key == curses.KEY_RIGHT:
direction = (1, 0)
elif key == curses.KEY_DOWN:
direction = (0, 1)
elif key == curses.KEY_UP:
direction = (0, -1)
elif key in (0x1b, ord("q")): # <ESC>
break
x += direction[0]
y += direction[1]
screen.addstr(y, x, b"*")
screen.refresh()
time.sleep(0.1)
curses.wrapper(main)
Now, I don’t even need to go into explaining any more - notice that the game of the little snake starts to appear naturally - and it’s just a matter of programming now. And, surprise: you don’t "redesign the whole screen" in this type of game - simply, add a character in the new position of the head of the hedgerow, leaving those who were already there - that’s how the game got fast on the old computers where it was created. (A modern game redesigns the entire scene, all frames, sometimes recalculating tens of thousands of polygons, Stretching and illuminating everything-if it’s a 3D game)
What we need for the game to be complete: make it cover up by "blacking out" after it has walked its size - and make it detect a collision. For this second part, we can still use a function of curses
to "see" the character in a given position - this exists - and it is what allowed the little games of old to be simpler than today. Why the correct today would be instead of "look" at the screen, check using the data structures we have, if the cover hit something - be a part of itself, a wall, an "apple" (which makes it increase in size and the game progress), etc... Mainly because if the game is in graphic mode, or even in 3D, it is not possible to "recognize" what is in a position where the cover moves. (in this case with the curses, we could see the character and use an "if" to know if it is a "@" representing an apple, or a "#" representing a wall, for example). This answer has already become a book chapter, but it doesn’t have to be the whole book - I’ll keep the simple case where we "look" at the screen with the method screen.instr(linha, coluna)
:
import curses
import locale
import time
locale.setlocale(locale.LC_ALL, '')
encoding = locale.getpreferredencoding()
def main(screen):
curses.curs_set(0)
screen.nodelay(True)
y, x = 20, 20
Direction = (1, 0)
while True:
key = screen.getch()
if key == curses.KEY_LEFT:
Direction = (-1, 0)
Elif key == curses.KEY_RIGHT:
Direction = (1, 0)
Elif key == curses.KEY_DOWN:
Direction = (0, 1)
Elif key == curses.KEY_UP:
Direction = (0, -1)
Elif key in (0x1b, Ord("q")): #
break
x += Direction[0]
y += Direction[1]
if screen.instr(y, x, 1) != b" ":
screen.addstr(10, 15, "You died!".NCODE(encoding))
screen.refresh()
time.Sleep(3)
break
screen.addstr(y, x, b"*")
screen.refresh()
time.Sleep(0.1)
curses.wrapper(main)
And finally, we create a desired initial length for the cover, store all the coordinates (x, y) it went through - and when it reached that length, erase the last "*" at that position. Thanks to the Python lists, it’s trivial to do this. (but you can learn more and do it more elegantly using the collections.deque
). That’s literally 6 more lines of code - and the game would be "complete", except it’s no fun at all. I take the opportunity to include the "apple" that increases the length of the covering in 5 strokes at a time. No secret either - we use the module "Random" to draw the apple. We put scores and a check of the screen limits (the curses exposes the terminal size in curses.COLS and curses.LINES) - I think that’s all there is to it:
import curses
import locale
import time
import random
locale.setlocale(locale.LC_ALL, '')
encoding = locale.getpreferredencoding()
def sorteia_maca(screen):
pos = random.randrange(0, curses.COLS), random.randrange(0, curses.LINES)
screen.addstr(pos[1], pos[0], b"@")
def main(screen):
curses.curs_set(0)
screen.nodelay(True)
y, x = 20, 20
direction = (1, 0)
comprimento = 5
corpo = []
sorteia_maca(screen)
pontuacao = 0
while True:
key = screen.getch()
if key == curses.KEY_LEFT:
direction = (-1, 0)
elif key == curses.KEY_RIGHT:
direction = (1, 0)
elif key == curses.KEY_DOWN:
direction = (0, 1)
elif key == curses.KEY_UP:
direction = (0, -1)
elif key in (0x1b, ord("q")): # <ESC>
break
x += direction[0]
y += direction[1]
corpo.insert(0, (x, y))
if len(corpo) > comprimento:
final = corpo.pop()
screen.addstr(final[1], final[0], b" ")
cabeca_em = screen.instr(y, x, 1)
if cabeca_em == b"@":
pontuacao += 5
comprimento += 5
sorteia_maca(screen)
elif cabeca_em != b" " or x >= curses.COLS or y > curses.LINES or x < 0 or y < 0:
screen.addstr(10, 15, "Você morreu! Pontos: {}".format(pontuacao).encode(encoding))
screen.refresh()
time.sleep(3)
break
screen.addstr(y, x, b"*")
screen.refresh()
time.sleep(0.1)
curses.wrapper(main)
Ready - completely playable! You can increment it with several things now - for example, by making the apple change place from time to time, you can put "levels" with "walls" and increasing the speed, you can switch to a graphical screen using "pygame" or another framework, instead of keeping itor in text mode, you can write a class in object orientation to cover - allowing you to have more instances of it on the same screen, the others being controlled by the computer (or - connect the game in network, and have a cover controlled by another player)
Make a Edit in the above answer, Problem and Explanation, and attach that answer.
– Edilson
Really? The platform Recommends 1 answer per question. But are two completely different things, and the text would get very large. If algebraic search is for "Python cover game" - the answer above is good. To answer the question of the OP, this is better - not I why not keep separate - if it was forbidden to have two answers from the same author, the platform simply would not allow.
– jsbueno
(Just so we’re clear, "Seriously?" that the comment above was not meant to be offensive - it was to ask if @Edilson really wanted me to do it. Only at the time of reading it seemed like an ill-mannered affront - sorry. Revisiting the answers now, it seems like it really makes sense to keep the two answers)
– jsbueno