2
\$\begingroup\$

I've developed "Element Fusion," a PyGame-based game inspired by 2048 but using chemical elements. I'm seeking feedback on:

  • Gameplay mechanics
  • Code structure and efficiency
  • PyGame implementation
  • User interface

The game features a 5x5 grid where players combine elements to create heavier ones.

import pygame
import random
import sys

pygame.init()

WIDTH, HEIGHT = 600, 700
GRID_SIZE = 5
CELL_SIZE = WIDTH // GRID_SIZE
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

ELEMENTS = [
    ('H', 'Hydrogen', 1, 1.008, (141, 182, 205)),
    ('He', 'Helium', 2, 4.003, (155, 205, 155)),
    ('Li', 'Lithium', 3, 6.941, (205, 155, 155)),
    ('Be', 'Beryllium', 4, 9.012, (205, 173, 0)),
    ('B', 'Boron', 5, 10.811, (238, 180, 34)),
    ('C', 'Carbon', 6, 12.011, (139, 69, 19)),
    ('N', 'Nitrogen', 7, 14.007, (144, 238, 144)),
    ('O', 'Oxygen', 8, 15.999, (70, 130, 180)),
    ('F', 'Fluorine', 9, 18.998, (238, 130, 238)),
    ('Ne', 'Neon', 10, 20.180, (255, 62, 150)),
    ('Na', 'Sodium', 11, 22.990, (171, 130, 255)),
    ('Mg', 'Magnesium', 12, 24.305, (50, 205, 50)),
    ('Al', 'Aluminum', 13, 26.982, (165, 42, 42)),
    ('Si', 'Silicon', 14, 28.086, (85, 107, 47)),
    ('P', 'Phosphorus', 15, 30.974, (255, 140, 0)),
]

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Element Fusion")

grid = [[None for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
score = 0

def move(direction):
    global score
    moved = False
    if direction in ('LEFT', 'RIGHT'):
        for y in range(GRID_SIZE):
            line = [grid[y][x] for x in range(GRID_SIZE) if grid[y][x]]
            if direction == 'RIGHT':
                line.reverse()
            new_line = []
            i = 0
            while i < len(line):
                if i + 1 < len(line) and line[i] == line[i+1]:
                    current_index = next((index for index, e in enumerate(ELEMENTS) if e[0] == line[i]), None)
                    if current_index is not None and current_index < len(ELEMENTS) - 1:
                        new_line.append(ELEMENTS[current_index + 1][0])
                        score += ELEMENTS[current_index + 1][2]
                    else:
                        new_line.append(line[i])
                    i += 2
                    moved = True
                else:
                    new_line.append(line[i])
                    i += 1
            new_line += [None] * (GRID_SIZE - len(new_line))
            if direction == 'RIGHT':
                new_line.reverse()
            for x in range(GRID_SIZE):
                if grid[y][x] != new_line[x]:
                    moved = True
                grid[y][x] = new_line[x]
    else:  # UP or DOWN
        for x in range(GRID_SIZE):
            line = [grid[y][x] for y in range(GRID_SIZE) if grid[y][x]]
            if direction == 'DOWN':
                line.reverse()
            new_line = []
            i = 0
            while i < len(line):
                if i + 1 < len(line) and line[i] == line[i+1]:
                    current_index = next((index for index, e in enumerate(ELEMENTS) if e[0] == line[i]), None)
                    if current_index is not None and current_index < len(ELEMENTS) - 1:
                        new_line.append(ELEMENTS[current_index + 1][0])
                        score += ELEMENTS[current_index + 1][2]
                    else:
                        new_line.append(line[i])
                    i += 2
                    moved = True
                else:
                    new_line.append(line[i])
                    i += 1
            new_line += [None] * (GRID_SIZE - len(new_line))
            if direction == 'DOWN':
                new_line.reverse()
            for y in range(GRID_SIZE):
                if grid[y][x] != new_line[y]:
                    moved = True
                grid[y][x] = new_line[y]
    return moved

def is_game_over():
    return all(all(cell is not None for cell in row) for row in grid)

def add_new_element():
    empty_cells = [(x, y) for x in range(GRID_SIZE) for y in range(GRID_SIZE) if grid[y][x] is None]
    if empty_cells:
        x, y = random.choice(empty_cells)
        grid[y][x] = random.choice(['H', 'He'])

def draw_grid():
    for y in range(GRID_SIZE):
        for x in range(GRID_SIZE):
            cell_rect = pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
            pygame.draw.rect(screen, (100, 100, 100), cell_rect, 1)
            
            if grid[y][x]:
                element = next((e for e in ELEMENTS if e[0] == grid[y][x]), None)
                if element:
                    # Draw element background
                    inner_rect = cell_rect.inflate(-10, -10)
                    pygame.draw.rect(screen, element[4], inner_rect, border_radius=int(CELL_SIZE * 0.15))
                    pygame.draw.rect(screen, WHITE, inner_rect, 2, border_radius=int(CELL_SIZE * 0.15))
                    
                    # Draw element symbol
                    font_size = int(CELL_SIZE * 0.4)
                    font = pygame.font.Font(pygame.font.match_font('arial'), font_size)
                    text = font.render(element[0], True, WHITE)
                    text_rect = text.get_rect(center=cell_rect.center)
                    screen.blit(text, text_rect)
                    
                    # Draw atomic number
                    font_size = int(CELL_SIZE * 0.2)
                    font = pygame.font.Font(pygame.font.match_font('arial'), font_size)
                    text = font.render(str(element[2]), True, WHITE)
                    text_rect = text.get_rect(topleft=(cell_rect.left + int(CELL_SIZE * 0.1), cell_rect.top + int(CELL_SIZE * 0.1)))
                    screen.blit(text, text_rect)
                    
                    # Draw atomic mass
                    text = font.render(f"{element[3]:.2f}", True, WHITE)
                    text_rect = text.get_rect(bottomright=(cell_rect.right - int(CELL_SIZE * 0.1), cell_rect.bottom - int(CELL_SIZE * 0.1)))
                    screen.blit(text, text_rect)

def draw_background():
    for y in range(HEIGHT):
        r = int(44 + (y / HEIGHT) * 26)
        g = int(62 + (y / HEIGHT) * 18)
        b = int(80 + (y / HEIGHT) * 20)
        pygame.draw.line(screen, (r, g, b), (0, y), (WIDTH, y))

def main():
    global score
    clock = pygame.time.Clock()
    add_new_element()
    add_new_element()

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                moved = False
                if event.key == pygame.K_LEFT:
                    moved = move('LEFT')
                elif event.key == pygame.K_RIGHT:
                    moved = move('RIGHT')
                elif event.key == pygame.K_UP:
                    moved = move('UP')
                elif event.key == pygame.K_DOWN:
                    moved = move('DOWN')
                
                if moved:
                    add_new_element()
                
                if is_game_over():
                    print(f"Game Over! Final Score: {score}")
                    pygame.quit()
                    sys.exit()

        draw_background()
        draw_grid()
        font = pygame.font.Font(pygame.font.match_font('arial'), 36)
        score_text = font.render(f"Score: {score}", True, WHITE)
        screen.blit(score_text, (10, HEIGHT - 40))
        pygame.display.flip()
        clock.tick(30)

if __name__ == "__main__":
    main()

Any suggestions to improve the game or the code would be greatly appreciated.

Edit:

I've updated the code and fixed some bugs. Also the GUI is improved. The updated code is on my GitHub.

enter image description here

enter image description here

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I don't write python, but can follow this code without difficulty. Kudos to you! Only suggestion is that move() contains two regions with duplicate lines of processing that possibly could be factored out into a function that is re-used... (A few comments wouldn't hurt, either, but that's your call...) Cheers! :-) \$\endgroup\$
    – Fe2O3
    Commented Jun 21 at 22:51
  • 1
    \$\begingroup\$ PS: [Worth knowing](merriam-webster.com/grammar/… :-) (link is wonky...) \$\endgroup\$
    – Fe2O3
    Commented Jun 21 at 22:54
  • \$\begingroup\$ @toolic I'm very good at coding, but for scientific stuff. I have zero experience with making software/games. I've created a github repo for it. I'd love to see this game on real devices (for free) but I don't know how to proceed from here. I also have some cool ideas to implement. If you like to contribute, please join me here: github.com/amirh0ss3in/Element-Fusion \$\endgroup\$ Commented Jun 29 at 14:14
  • 1
    \$\begingroup\$ Pix look good. Thumbs up! It's probably of no importance in python, but, for the record, if you ever convert this to, for instance, 'C', you might consider moving the full name of each element from actual data into comments on each row... There's no need to 'clutter' a binary executable with irrelevant, unused data... \$\endgroup\$
    – Fe2O3
    Commented Jul 4 at 12:06

2 Answers 2

2
\$\begingroup\$

The GUI looks great, the game is a lot of fun to play and (unfortunately) it is quite addictive.

Bug or feature

After playing it a hundred times or so, I began to notice that the game was ending earlier than I expected. It was hard to tell for sure because the GUI closes abruptly when the game ends. One feature request is to keep the GUI window open when the game ends, and add the capability for the user to close the window.

According to the 2048 Wikipedia:

When the player has no legal moves (there are no empty spaces and no adjacent tiles with the same value), the game ends.

Your game ends as soon as there are no empty spaces, even if there are still adjacent tiles with the same value. It would be a nice feature to keep the game going while there are still legal moves.

To demonstrate my point, I commented out the lines that close the window:

            if is_game_over():
                print(f"Game Over! Final Score: {score}")
                #pygame.quit()
                #sys.exit()

Then I intentionally tried to end the game quickly by alternating the up and down arrows (this took about 50 key presses). I had a problem uploading the image into this answer, but there are several legal left/right moves still remaining.

Currently, the is_game_over function only checks if no spaces are empty. It would need to also check if moves can be made.

If that change is made, I just might be able to get to Aluminum!

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for your kind words... it means a lot. I will fix this bug and change the source code on the game's GitHub. \$\endgroup\$ Commented Jul 2 at 22:50
  • \$\begingroup\$ I've updated the code on GitHub! @toolic \$\endgroup\$ Commented Jul 4 at 11:16
2
\$\begingroup\$

Overview

The code layout is good, and you used meaningful names for classes, functions and variables.

Documentation

The code should have a header comment describing the purpose of the code, such as:

"""
Element Fusion is a game inspired by 2048 but using chemical elements.
The game features a 5x5 grid where players combine elements to create heavier ones.
"""

The functions could benefit from doctrings briefly describing their purpose and return value (if any):

def move(direction):
    """ Moves elements.  Returns boolean True if move was successful. """

Usage

When I run the code, it is not clear what it expects me to do. There should be some instructions printed in the GUI, such as:

Use the arrow keys on your keyboard to move the elements:
    UP, DOWN, RIGHT, LEFT
For more information, refer to ...

Layout

The data would be easier to understand and maintain if you aligned the columns and added a comment for the column headings:

ELEMENTS = [
    # Symbol   Name       Number  Mass
    ('H'    , 'Hydrogen' , 1    , 1.008 , (141 , 182 , 205)),
    ('He'   , 'Helium'   , 2    , 4.003 , (155 , 205 , 155)),

I suspect the last triplet is the RGB color.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.