I like this code.
It accomplishes a simple goal in a simple way, very direct and clear.
Conforming to pep8 and using Enum is very nice.
It reminds me of POKE -16298, 0 for LORES graphic mode.
Let's examine low level details first.
__main__
guard
pygame.init()
...
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
screen.fill((255,) * 3)
I'm not crazy about doing a bunch of stuff at top level.
Prefer to protect such statements with a main guard.
The standard idiom is:
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
Why do we do this?
Well, I understand that no file has an import scrolling_game
statement, yet.
But we anticipate that could happen in future.
For example if you write a
unit test
or use a couple of modules to implement a fancier game.
The idea is that another file should be able to import
this one with no side effects, e.g. print()
statements
or game windows opening.
So class / function definitions are fair game,
plus constants like width / height.
But the meat of the application typically goes into
def main():
which we run according to what
the module __name__
is.
Notice that, for someone importing this module,
the module name will look like "scrolling_game"
rather than "__main__"
.
That third statement, which blanks out the screen with white,
appears to be leftover from initial development / debug,
and should just be deleted.
Once the main display loop begins it becomes superfluous.
And defining a WHITE
manifest constant
wouldn't hurt.
cells dictate the display
def draw_map():
...
match tile:
case TileTypes.AIR:
pass
while not done:
...
screen.fill((255,) * 3) # <-- recommend you delete this
draw_map()
I'm not keen on the pass
, there.
Prefer to repaint every tile, every time,
rather than defaulting all tiles to AIR
followed by selective repaint of non-AIR tiles.
The current code makes sense, but I will
introduce an alternate repainting strategy below.
Also, the match
works out fine.
But it would be better to put a color attribute
on each Enum value, so we could unconditionally
do lookup + render.
use an array
map_layout = [[TileTypes.AIR for j in range(MAP_WIDTH)]
for i in range(MAP_HEIGHT)]
In Lisp and in Python a list-of-lists datastructure
can be a very nice representation of N items
that are "all the same thing", like tiles.
But it suffers from chasing N object pointers,
which aren't cache friendly.
CPU hardware is good at predicting and prefetching
sequential reads, but less so for random reads.
Prefer to represent N tiles with a vector, perhaps an
array or an
ndarray.
That leaves us with something like
map_layout = np.ones((MAP_WIDTH, MAP_HEIGHT)) * TileTypes.AIR
slice when scrolling
A central concern is how to efficiently move the grid by
one tile in any direction.
Let's start with a level that spans several screens:
level = np.ones((5 * MAP_WIDTH, 3 * MAP_HEIGHT)) * TileTypes.AIR
Pure AIR would be boring, so fill in additional level tiles to taste.
Now let's render a portion of that level:
x, y = get_current_position()
map_layout = level[x : x+MAP_WIDTH][y : y+MAP_HEIGHT]
We can advance x
or y
by +/- 1 pixel
and re-render to scroll to a new spot within the level.
Rather than interpreted bytecode we are looping in C,
so it's essentially MAP_HEIGHT memcpy()'s per screen refresh,
with no random reads from chasing object pointers.
If all levels have similar dimensions, then you could
move from two dimensions to a three-dimensional array:
level[lvl][x][y]
This is solid code.
I would be happy to delegate or accept maintenance tasks for it.
Recommend the use of array slicing for frame updates.