I'm making a platformer game called Uni where this small character has to go as high as possible, in which the player constantly has to choose between jumping (W) going left or right (A-D) climbing down (S) or waiting (Just enter). A friend wanted a very difficult game that requires not timing but thinking to progress. Hence there are many rules that players learn throughout the game:
You may jump two characters over where you were initially at.
To go over a block, you must jump higher than it and from below.
To go over a ladder, you must jump at least as high as where it is from below.
When in the air, you can change direction (A-D) or you can stay where you are at.
When falling, you catch up speed.
Because it is a game based on Frames per Input, the number of "you"s you see represents the speed at which you just moved to reach your current position.
If you jump over a ladder exactly 3 characters above you, its as if you jumped over a block 2 characters above you.
If you jump over a ladder less than 3 characters above you, the next time you press enter you will "bounce" one character higher, because of the exess of velocity.
When you enter contact with a checkpoint, it activates it. When you die, you get teleported to the last checkpoint you activated (if any) and that same one dissappears. If there are no more activated checkpoints left, you simply die.
So the game works, and is very difficult but is possible. When I made it though, I wished for the layout to be randomly generated each time the script is runned. Now I realise that with so many rules, I have no idea how to get the layout to not just be randomly generated but also possible. The current game has a static layout.
Here is the script (although kinda sloppy):
#Mechanics:
#Make a list for each printed line. When the player goes higher, shift the list down 1 and add a new layer
import os
import time
debug = False
def get_key():
try:
import sys, tty, termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
except:
return input("Press a key and hit Enter: ")[0]
checkpoint1 = "\u235A"
my_list = []
# 18 spaces between sides
# U+2223, U+2336, U+2180, U+2182, U+2188
portal = " "
def updater():
global my_list
global portal
my_list = [
" ",
" ",
" ",
" ",
f" \u2359 \u2359 {portal} ",
#" \u2359 \u210D \u210D\u20AA ",
" \u2359 \u210D \u210D\u20AA\u20AA\u20AA\u20AA\u20AA\u20AA\u20AA\u20AA\u20AA",
" \u210D\u20AA \u2359 \u210D\u20AA ",
f" \u210D ",
" \u210D\u20AA\u20AA\u2359\u2359\u210D ",
" \u210D ",
" \u20AA \u20AA\u210D\u20AA ",
" \u2359\u210D \u2359 ",
" \u20AA \u2359 \u20AA\u20AA\u2359 ",
f" \u210D {checkpoint1} \u2359 ",
" \u20AA\u20AA\u20AA\u20AA\u20AA\u20AA\u20AA\u20AA ",
"_\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359\u2359_",
" ",
" ",
" ",
" ",
" ",
" "
]
updater()
# Number of visible lines
window_height = 9
scrollingY = 0
playerX = 1
player = '\u237E'
fellat = 0
dead = False
while scrollingY + window_height < len(my_list):
scrollingY += 1
scrollingY -= 6
def clear():
os.system('cls' if os.name == 'nt' else 'clear')
s = []
middle = 0
playerline = 0
target = "a"
undertarget = "b"
underundertarget = "c"
fellat = 0
key = "uhh"
def display_list():
global target
global undertarget
global underundertarget
global s
global playerX
global middle
global fellat
clear()
global scrollingY
global player
global dead
global playerline
global checkpoint1
global key
global portal
setrightundertarget = False
for i in range(scrollingY, min(scrollingY + window_height, len(my_list))):
if i == min(scrollingY+window_height,len(my_list))-3:
#print the list with the player at the center
playerline = i
s = my_list[i]
middle = len(s) // 2
# Split around the middle
before = s[:middle+playerX]
target = s[middle+playerX]
after = s[(middle+playerX+1):]
undertarget = my_list[i+1][((len(my_list[i+1])//2)+playerX)]
if setrightundertarget == False:
rightundertarget = undertarget
setrightundertarget = True
#Check if player reached checkpoint1 and checkpoint1 is deactivated
if target == "\u235A" and scrollingY == 7 and playerX == -3:
checkpoint1 = "\u06E9"
updater()
#Check if player is inside ladder but not going down(bring back on top),
#else if player is trying to go down a floor(bring back on top)
#else if player is over or in a spike (death),
#else if player is over nothing or a checkpoint (fall),
#else since player is not falling if player is about to bounce (prevent)
#Bouncing occurs after going over a regular block without having fellat at 0
if (target == "\u210D" and key != "s") or (target == "\u20AA" and key == "s"):
scrollingY -= 1
fellat = 0
clear()
display_list()
break
elif undertarget == "\u2359" or target == "\u2359":
player = "\u237D"
dead = True
#Player is dead from spike
elif undertarget == " " or undertarget == "\u235A" or undertarget == "\u06E9":
scrollingY += 1
elif rightundertarget == "\u20AA" and fellat == 2:
#Bouncing occurs after going over a regular block without having fellat at 0
fellat = 0
# Print the original middle character, backspace, then overwrite it with the player.
print(before + target + '\b' + player + after)
else:
print(my_list[i])
if dead == True:
print("\n GAME OVER ")
while dead == False or checkpoint1 == "\u06E9":
clear()
scrollingY -= fellat
display_list()
if debug:
print(f"PlayerX:{playerX} and ScrollingY:{scrollingY}\ntarget:{target}\nundertarget:{undertarget}\nfellat:{fellat}")
key = input(">> ").lower()
if checkpoint1 == "\u06E9" and dead == True:
checkpoint1 = " "
updater()
dead = False
fellat = 0
playerX = -3
scrollingY = 7
clear()
display_list()
player = "\u237E"
if key == 'q':
break
elif key == 'w': # and scrollingY > 0:
fellat = 3
elif key == 's' and scrollingY + window_height < len(my_list):
scrollingY += 1
fellat = 0
elif key == 'd' and playerX < 7 and my_list[playerline-(1 if fellat != 0 else 0)][(middle+playerX)+1] != "\u20AA":
playerX += 1
elif key == 'a' and playerX > (0-10) and my_list[playerline-(1 if fellat != 0 else 0)][(middle+playerX)-1] != "\u20AA":
playerX -= 1
if fellat > 0:
fellat -= 1
if scrollingY == -2 and portal == " ":
# U+2223, U+2336, U+2180, U+2182, U+2188
for i in ["\u2223","\u2336","\u2180","\U00002182"]:
portal = i
updater()
clear()
display_list()
time.sleep(0.15)
if playerX == 6 and scrollingY == -2:
playerlooksbefore = player
player = " "
updater()
clear()
display_list()
time.sleep(0.2)
for i in ["\u2180", "\u2336", "\u2223", " "]:
portal = i
updater()
clear()
display_list()
time.sleep(0.2)
time.sleep(1.8)
clear()
time.sleep(6)
for i in [" ", "\u2223","\u2336","\u2180", "\U00002182", playerlooksbefore]:
clear()
player = i
r,c=os.get_terminal_size()
print("\n"*r + " "*(c//2) + f"{player}")
time.sleep(0.2)
time.sleep(2.8)
clear()
print("[The Creator] - Congradulations.")
time.sleep(2)
blah = input("<<Continue>>")
clear()
print("[The Creator] - You have proven yourself worthy of becoming citizen of unicode.")
time.sleep(2)
blah = input("<<Continue>>")
for i in [player, "\U00002182", "\u2180", "\u2336","\u2223", " "]:
clear()
player = i
r, c = os.get_terminal_size()
print("\n"*r + " "*(c//2) + f"{player}")
time.sleep(0.9)
time.sleep(3)
clear()
time.sleep(4)
print("YOU WON")
time.sleep(0.5)
for i in ["\n","Coded by:","Emmanuel Ghattas","\n",f"Thanks for pl{playerlooksbefore}ying!"]:
print(i)
time.sleep(0.2)
time.sleep(7)
break
If anyone knows how to get a layout that changes each time the player runs the script while keeping it playable, or simply has ideas for the game, please let me know.
-
\$\begingroup\$ Sorry for the messy script, I rewrote it and re-asked the same question, please go to: codereview.stackexchange.com/questions/299588/… \$\endgroup\$Chip01– Chip012025年11月18日 21:27:31 +00:00Commented Nov 18 at 21:27
3 Answers 3
Notes
- Avoid global variables. You use them extensively, but it makes your logic much harder to follow. Information should be passed via function arguments, back out via return values, or can be encapsulated in objects which are handled this way.
- Aim for small functions that do specific things, do them well, and can be composed together to accomplish larger goals. If you can't view the entire function on a single page, it's too long. If you can't sum up what the function does in a concise docstring1, it's doing too much.
- Many of your lines are much too long. Aim for an 80 character width limit.
- Comparisons to
TrueorFalseare unnecessary.dead == Falseis the same asnot dead. - Avoid repeating yourself. Near the beginning of
display_listyou calculatemin(scrollingY + window_height, len(my_list))twice when this could have been calculated once and stored in a variable. - Avoid "magic numbers" in your code, favoring named constants instead.
- Do not write a catch-all
except:as you have. Instead aim to catch specific exception types.
I strongly suggest you read PEP 8. This is the style guide for Python code.
I'm not going to comment on the functionality of your code because the style issues need to be addressed to make it understandable.
1 Read PEP 257 on docstrings.
Initial Impression
- The first thing I did was to try running your program to get a feel for what it is doing. But I was frustrated because, although you posted instructions of a sort in your post, I was given no prompts in your actual code as to what I can enter. Moreover, although you say in your instructions, "You may jump two characters over where you were initially at", I have no idea who "I" am or where I am initially or what the characters on the screen I am looking at (H's and ?'s) even mean.
- If I enter a command that is not valid, I would like to see some sort of error message.
- Looking at the code offers no insight as to what I as a player should be doing for lack of comments.
So Many Globals!
Python supports object-oriented programming including encapsulation. I think readability would be improved if you had a class named Uni that had an __init__ method that initialized a new game, i.e. initialized attributes that are currently global variables and included private methods (e.g. _get_key, _updater, etc.) and public methods (e.g. run_game). This would also help make the code more importable without having any unwanted side-effects.
There may be some duplications of what others have already said:
Docstrings Please!
Please document what the module does by including a docstring at the top or, if you go the object-oriented route, at the class level. Having docstrings for at least the public functions (or methods) would be useful.
Be Pythonic and Efficient
- You have a variable
deadthat takes on the valuesTrueandFalseat various points in the code. But then you have tests such asif test == True:andif test == False:, which would be more pythonic if expressed respectively asif test:andif not test:. - A test such as
if undertarget == " " or undertarget == "\u235A" or undertarget == "\u06E9":can be more concisely and efficiently expressed asif undertarget in (" ", "\u235A", "\u06E9"):. - You have
importstatements within functions that are called multiple times. True, the actual import will only occur once, but the test to see whether the modules have already been imported will be performed needlessly multiple times. Move theseimportstatement to the top of your file.
Layout
Move the functions to the top after the import lines.
Having them in the middle of the code interrupts the natural
flow of the code (from a human readability standpoint).
Also, add blank line before and after the functions. The black program can be used to automatically reformat the code. This will also add consistency to you other whitespace usage.
Documentation
We don't know what the code does because you did not tell us. You shoud summarize the purpose of the code at the top using a docstring such as:
"""
A platformer game called Uni where this small character has to go as high as possible,
in which the player constantly has to choose between jumping (W)
going left or right (A-D) climbing down (S) or waiting (Just enter).
Add details here.
"""
The PEP 8 style guide recommends adding docstrings for functions as well.
Simpler
Lines like this:
if dead == True:
are simpler as:
if dead:
There is no need to explicitly compare against boolean values True and False.
You can use the
ruff
tool to identify all such cases.
Naming
The PEP 8 style guide recommends snake_case for function and variable names.
setrightundertarget would be set_right_under_target.
DRY
This code has 3 comparisons to the same variable:
elif undertarget == " " or undertarget == "\u235A" or undertarget == "\u06E9":
This is simpler and removes the repetition:
elif undertarget in (" ", "\u235A", "\u06E9"):
UX
When I run the code, I seem to be prompted to do something, but I don't know what I am expected to do. Instead of just showing me this bare prompt:
key = input(">> ").lower()
perhaps you could print some simple instructions.
-
1\$\begingroup\$
blackis pretty great, though I've also become a fan orrufffor having bothruff formatandruff check(with optional--fix), where the former roughly replacesblackfor formatting and the latterisortandflake8. It's also impressively faster at both of those. \$\endgroup\$fyrepenguin– fyrepenguin2025年11月19日 06:02:27 +00:00Commented Nov 19 at 6:02 -
1\$\begingroup\$ @fyrepenguin: Agreed! I've recommended
ruffin 73 of my answers already, so I'm sold. \$\endgroup\$toolic– toolic2025年11月19日 12:33:17 +00:00Commented Nov 19 at 12:33 -
1\$\begingroup\$ @fyrepenguin: I tried avoiding it as per the rules, but I think its worth saying: THANKS \$\endgroup\$Chip01– Chip012025年11月20日 23:33:11 +00:00Commented Nov 20 at 23:33