2048 Game (Terminal)

import os

import random

import json

import copy

from datetime import datetime

from pathlib import Path


# ============================================================

# CONFIGURATION

# ============================================================


SAVE_FILE      = "2048_save.json"

HIGH_SCORE_FILE = "2048_scores.json"

GRID_SIZE      = 4


# ============================================================

# COLORS (ANSI — works on Linux/Mac and Windows 10+)

# ============================================================


TILE_COLORS = {

    0:     "\033[90m",      # dark gray

    2:     "\033[97m",      # white

    4:     "\033[93m",      # yellow

    8:     "\033[33m",      # dark yellow

    16:    "\033[91m",      # light red

    32:    "\033[31m",      # red

    64:    "\033[95m",      # magenta

    128:   "\033[94m",      # light blue

    256:   "\033[34m",      # blue

    512:   "\033[96m",      # cyan

    1024:  "\033[36m",      # dark cyan

    2048:  "\033[92m",      # bright green

}

RESET  = "\033[0m"

BOLD   = "\033[1m"


def tile_color(val):

    return TILE_COLORS.get(val, "\033[92m")


# ============================================================

# HIGH SCORE

# ============================================================


def load_high_scores():

    if Path(HIGH_SCORE_FILE).exists():

        try:

            with open(HIGH_SCORE_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return []



def save_high_score(score, moves):

    scores = load_high_scores()

    scores.append({

        "score": score,

        "moves": moves,

        "date":  datetime.now().strftime("%d-%m-%Y %H:%M"),

    })

    scores = sorted(scores, key=lambda x: x["score"], reverse=True)[:10]

    with open(HIGH_SCORE_FILE, "w") as f:

        json.dump(scores, f, indent=2)



def show_high_scores():

    scores = load_high_scores()

    print("\n" + "="*45)

    print("  TOP 10 HIGH SCORES")

    print("="*45)

    if not scores:

        print("  No scores yet. Play a game!")

    else:

        print(f"  {'#':<4} {'SCORE':>10}  {'MOVES':>6}  DATE")

        print("  " + "-"*40)

        for i, s in enumerate(scores, 1):

            print(f"  {i:<4} {s['score']:>10,}  {s['moves']:>6}  {s['date']}")

    print("="*45)



# ============================================================

# SAVE / LOAD GAME

# ============================================================


def save_game(board, score, moves):

    with open(SAVE_FILE, "w") as f:

        json.dump({"board": board, "score": score, "moves": moves}, f)

    print("  Game saved!")



def load_game():

    if Path(SAVE_FILE).exists():

        try:

            with open(SAVE_FILE, "r") as f:

                data = json.load(f)

            return data["board"], data["score"], data["moves"]

        except:

            pass

    return None, None, None



# ============================================================

# BOARD OPERATIONS

# ============================================================


def new_board():

    board = [[0] * GRID_SIZE for _ in range(GRID_SIZE)]

    board = add_tile(board)

    board = add_tile(board)

    return board



def add_tile(board):

    empty = [(r, c) for r in range(GRID_SIZE)

             for c in range(GRID_SIZE) if board[r][c] == 0]

    if empty:

        r, c = random.choice(empty)

        board[r][c] = 4 if random.random() < 0.1 else 2

    return board



def slide_row_left(row):

    """Slide and merge a single row to the left. Returns (new_row, score_gained)."""

    tiles  = [x for x in row if x != 0]

    merged = []

    score  = 0

    i = 0

    while i < len(tiles):

        if i + 1 < len(tiles) and tiles[i] == tiles[i + 1]:

            val = tiles[i] * 2

            merged.append(val)

            score += val

            i += 2

        else:

            merged.append(tiles[i])

            i += 1

    merged += [0] * (GRID_SIZE - len(merged))

    return merged, score



def move_left(board):

    new_board = []

    score = 0

    for row in board:

        new_row, s = slide_row_left(row)

        new_board.append(new_row)

        score += s

    return new_board, score



def move_right(board):

    flipped = [row[::-1] for row in board]

    moved, score = move_left(flipped)

    return [row[::-1] for row in moved], score



def transpose(board):

    return [list(row) for row in zip(*board)]



def move_up(board):

    t = transpose(board)

    moved, score = move_left(t)

    return transpose(moved), score



def move_down(board):

    t = transpose(board)

    moved, score = move_right(t)

    return transpose(moved), score



def board_changed(old, new):

    return any(old[r][c] != new[r][c]

               for r in range(GRID_SIZE)

               for c in range(GRID_SIZE))



def is_game_over(board):

    """Check if no moves are possible."""

    # Any empty cell?

    for r in range(GRID_SIZE):

        for c in range(GRID_SIZE):

            if board[r][c] == 0:

                return False

    # Any adjacent equal cells?

    for r in range(GRID_SIZE):

        for c in range(GRID_SIZE):

            val = board[r][c]

            if c + 1 < GRID_SIZE and board[r][c + 1] == val:

                return False

            if r + 1 < GRID_SIZE and board[r + 1][c] == val:

                return False

    return True



def has_won(board):

    return any(board[r][c] >= 2048

               for r in range(GRID_SIZE)

               for c in range(GRID_SIZE))



def max_tile(board):

    return max(board[r][c]

               for r in range(GRID_SIZE)

               for c in range(GRID_SIZE))



# ============================================================

# DISPLAY BOARD

# ============================================================


def clear():

    os.system("cls" if os.name == "nt" else "clear")



def display_board(board, score, best, moves):

    clear()

    cell_w = 7


    print("\n" + "="*38)

    print(f"  {BOLD}2048{RESET}         "

          f"Score: {BOLD}{score:>7,}{RESET}  Best: {best:>7,}")

    print(f"  Moves: {moves:<6}   Max tile: {max_tile(board)}")

    print("="*38)


    # Top border

    border = "  +" + (("-" * cell_w + "+") * GRID_SIZE)

    print(border)


    for row in board:

        # Tile values

        line = "  |"

        for val in row:

            color = tile_color(val)

            if val == 0:

                line += f"{' ':^{cell_w}}|"

            else:

                val_str = str(val)

                padded  = val_str.center(cell_w)

                line   += f"{color}{BOLD}{padded}{RESET}|"

        print(line)

        print(border)


    print()

    print("  W/↑=Up  S/↓=Down  A/←=Left  D/→=Right")

    print("  P=Save  Q=Quit  N=New Game  H=High Scores")



# ============================================================

# UNDO SUPPORT

# ============================================================


class UndoStack:

    def __init__(self, max_size=5):

        self.stack    = []

        self.max_size = max_size


    def push(self, board, score):

        self.stack.append((copy.deepcopy(board), score))

        if len(self.stack) > self.max_size:

            self.stack.pop(0)


    def pop(self):

        if self.stack:

            return self.stack.pop()

        return None, None


    def can_undo(self):

        return len(self.stack) > 0



# ============================================================

# GAME LOOP

# ============================================================


def game_loop(board=None, score=0, moves=0, best=0):

    if board is None:

        board = new_board()


    undo = UndoStack()


    while True:

        display_board(board, score, max(score, best), moves)


        # Win check

        if has_won(board):

            print(f"\n  YOU REACHED 2048! Congratulations!")

            cont = input("  Keep playing? (y/n): ").strip().lower()

            if cont != "y":

                save_high_score(score, moves)

                return score


        # Game over check

        if is_game_over(board):

            print(f"\n  GAME OVER! Final score: {score:,}")

            save_high_score(score, moves)

            return score


        # Can undo hint

        if undo.can_undo():

            print(f"  U=Undo ({len(undo.stack)} available)", end="  ")

        print()


        try:

            key = input("  Move: ").strip().lower()

        except (EOFError, KeyboardInterrupt):

            print("\n  Game interrupted.")

            save_high_score(score, moves)

            return score


        if key in ("q", "quit"):

            print(f"\n  Final score: {score:,}  Moves: {moves}")

            save_high_score(score, moves)

            return score


        elif key == "n":

            confirm = input("  Start new game? (y/n): ").strip().lower()

            if confirm == "y":

                save_high_score(score, moves)

                return game_loop(best=max(score, best))


        elif key == "p":

            save_game(board, score, moves)

            continue


        elif key == "h":

            show_high_scores()

            input("  Press Enter to continue...")

            continue


        elif key == "u":

            prev_board, prev_score = undo.pop()

            if prev_board:

                board = prev_board

                score = prev_score

                moves = max(0, moves - 1)

                print("  Undone!")

            else:

                print("  Nothing to undo.")

            continue


        # Moves

        move_map = {

            "w": move_up,    "up":    move_up,

            "s": move_down,  "down":  move_down,

            "a": move_left,  "left":  move_left,

            "d": move_right, "right": move_right,

        }


        move_fn = move_map.get(key)

        if not move_fn:

            continue


        # Save state for undo

        undo.push(board, score)


        new_b, gained = move_fn(board)


        if board_changed(board, new_b):

            board  = add_tile(new_b)

            score += gained

            moves += 1

        else:

            undo.pop()   # no change — remove saved state


    return score



# ============================================================

# MAIN MENU

# ============================================================


def main():

    best = 0

    scores = load_high_scores()

    if scores:

        best = scores[0]["score"]


    while True:

        clear()

        print("\n" + "="*38)

        print(f"  {BOLD}  2048  —  Terminal Edition{RESET}")

        print("="*38)

        print(f"  Best score: {best:,}\n")

        print("  1. New Game")

        print("  2. Continue saved game")

        print("  3. High Scores")

        print("  4. How to play")

        print("  0. Exit")

        print("="*38)


        choice = input("\n  > ").strip()


        if choice == "1":

            result = game_loop(best=best)

            best   = max(best, result)


        elif choice == "2":

            board, score, moves = load_game()

            if board:

                print(f"\n  Loaded game! Score: {score:,}  Moves: {moves}")

                input("  Press Enter to continue...")

                result = game_loop(board, score, moves, best)

                best   = max(best, result)

            else:

                print("  No saved game found.")

                input("  Press Enter...")


        elif choice == "3":

            show_high_scores()

            input("\n  Press Enter to return...")


        elif choice == "4":

            clear()

            print("\n" + "="*50)

            print("  HOW TO PLAY 2048")

            print("="*50)

            print("""

  Goal: Combine tiles to reach the 2048 tile!


  Controls:

    W / ↑   — Slide tiles UP

    S / ↓   — Slide tiles DOWN

    A / ←   — Slide tiles LEFT

    D / →   — Slide tiles RIGHT

    U       — Undo last move (up to 5 times)

    P       — Save game

    N       — New game

    H       — High scores

    Q       — Quit


  Rules:

    • Tiles with the same number merge when they collide.

    • Each merge doubles the tile value and adds to score.

    • A new tile (2 or 4) appears after each move.

    • Game ends when no moves are possible.

    • Reach 2048 to WIN — but keep going for a higher score!


  Scoring:

    • Each merge = merged tile value added to score.

    • e.g. merging two 64s = +128 points.

            """)

            input("  Press Enter to return...")


        elif choice == "0":

            print("\n  Thanks for playing! Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

No comments: