From 68c77a490ad92af7965ecdbc4a512d1d1e7fd7dc Mon Sep 17 00:00:00 2001 From: Yilun Wu Date: Mon, 6 Jul 2026 18:32:39 -0400 Subject: [PATCH 1/2] Restructure into src/test package layout and improve UI - Move board.py/AI.py/RL.py/UI.py into an installable src/game2048 package (pyproject.toml, setuptools src-layout, play2048 entry point); move tests into test/ with real pytest assertions (test_board.py, test_ai.py) replacing the old __main__ print-based checks in board.py and the random game-simulation block in AI.py - UI: reposition Score/Step/Time in the title bar and always show both Step and Time in both play modes; fix the AI-mode timer to track real elapsed time instead of counting AI move steps, with mm:ss.mmm precision - UI: restyle the customize-size and level-selection pages to match the start page's theme (scaled fonts, consistent palette), sharing layout/font-scale helpers instead of duplicated magic numbers - UI: add Esc to return to the home page from a paused or finished game - Add README with install/run/test instructions - Add .gitignore for pycache/egg-info build artifacts, drop stale tracked .pyc files Co-Authored-By: Claude Sonnet 5 --- 2048/.gitignore | 4 + 2048/__pycache__/AI.cpython-36.pyc | Bin 6877 -> 0 bytes 2048/__pycache__/AI.cpython-38.pyc | Bin 8893 -> 0 bytes 2048/__pycache__/AI.cpython-39.pyc | Bin 8842 -> 0 bytes 2048/__pycache__/UI.cpython-36.pyc | Bin 11913 -> 0 bytes 2048/__pycache__/UI.cpython-38.pyc | Bin 11567 -> 0 bytes 2048/__pycache__/UI.cpython-39.pyc | Bin 11547 -> 0 bytes 2048/__pycache__/board.cpython-36.pyc | Bin 6720 -> 0 bytes 2048/__pycache__/board.cpython-38.pyc | Bin 6655 -> 0 bytes 2048/__pycache__/board.cpython-39.pyc | Bin 6648 -> 0 bytes 2048/__pycache__/image_util.cpython-36.pyc | Bin 1167 -> 0 bytes 2048/pyproject.toml | 17 ++ 2048/{ => src/game2048}/AI.py | 57 +----- 2048/{ => src/game2048}/RL.py | 2 +- 2048/{ => src/game2048}/UI.py | 148 +++++++++----- 2048/src/game2048/__init__.py | 0 2048/{ => src/game2048}/board.py | 69 ------- 2048/{2048.py => src/game2048/main.py} | 7 +- 2048/test/test_ai.py | 114 +++++++++++ 2048/test/test_board.py | 227 +++++++++++++++++++++ README.md | 60 ++++++ 21 files changed, 530 insertions(+), 175 deletions(-) create mode 100644 2048/.gitignore delete mode 100644 2048/__pycache__/AI.cpython-36.pyc delete mode 100644 2048/__pycache__/AI.cpython-38.pyc delete mode 100644 2048/__pycache__/AI.cpython-39.pyc delete mode 100644 2048/__pycache__/UI.cpython-36.pyc delete mode 100644 2048/__pycache__/UI.cpython-38.pyc delete mode 100644 2048/__pycache__/UI.cpython-39.pyc delete mode 100644 2048/__pycache__/board.cpython-36.pyc delete mode 100644 2048/__pycache__/board.cpython-38.pyc delete mode 100644 2048/__pycache__/board.cpython-39.pyc delete mode 100644 2048/__pycache__/image_util.cpython-36.pyc create mode 100644 2048/pyproject.toml rename 2048/{ => src/game2048}/AI.py (91%) rename 2048/{ => src/game2048}/RL.py (99%) rename 2048/{ => src/game2048}/UI.py (72%) create mode 100644 2048/src/game2048/__init__.py rename 2048/{ => src/game2048}/board.py (80%) rename 2048/{2048.py => src/game2048/main.py} (72%) create mode 100644 2048/test/test_ai.py create mode 100644 2048/test/test_board.py diff --git a/2048/.gitignore b/2048/.gitignore new file mode 100644 index 0000000..2ec1dba --- /dev/null +++ b/2048/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.pytest_cache/ +*.egg-info/ diff --git a/2048/__pycache__/AI.cpython-36.pyc b/2048/__pycache__/AI.cpython-36.pyc deleted file mode 100644 index 337f0eaff6ee5996da77298d377d02a86e67ed2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6877 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 QCpA*6kMNJ4tSE@z0Gqd3qyPW_ diff --git a/2048/__pycache__/AI.cpython-39.pyc b/2048/__pycache__/AI.cpython-39.pyc deleted file mode 100644 index 064ba0c7948445e9bb909908aad86becdf4a6b7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8842 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 A-2eap diff --git a/2048/__pycache__/UI.cpython-38.pyc b/2048/__pycache__/UI.cpython-38.pyc deleted file mode 100644 index 822390c5efa4af57969021234e3da2012e616973..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11567 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 Gtp5X^->tg< diff --git a/2048/__pycache__/UI.cpython-39.pyc b/2048/__pycache__/UI.cpython-39.pyc deleted file mode 100644 index b460081d9a9b2fa738f99221884419286a7a6129..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11547 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 diff --git a/2048/__pycache__/board.cpython-38.pyc b/2048/__pycache__/board.cpython-38.pyc deleted file mode 100644 index fa89577df72a38e8f34428ad3edcd3ca1bec90b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6655 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 diff --git a/2048/__pycache__/board.cpython-39.pyc b/2048/__pycache__/board.cpython-39.pyc deleted file mode 100644 index 8fce823cad0fc4b099d73d73e5dcc76140dfa11f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6648 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 diff --git a/2048/__pycache__/image_util.cpython-36.pyc b/2048/__pycache__/image_util.cpython-36.pyc deleted file mode 100644 index a0a869bfb53365d15f76187851b7809032e13b52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1167 zcmYjQJC7ST5awfFT1j{3gDV?p(y6c&B!UqbK@cF0VFQM8urCJ86pKBPmUeF+3dyxj z0k@Ld{D3s+^Apmf@K12b)|LN4stmb4+g!{8!+CMO*>9%P^zC2&{Q75r&^z?mp`iZ& zr0xUbh~o+|>?L8uOUfunQU!I$LX0@&!E3}K9`le#uQ7``euierdx%HN8Mf)!rq%7^ zI$w(wc1AcM5L8A#0;#9KIMk?6kG;e_F}Nfp<%H7%nIO5)fgwEL=s@MdkTNtRi*#Hj z<;b7|?x~?=dO&@k$K}KX2iHNkl&&1RarWZ;(a+0A=jUh7p8xdp@xvDu*<0C4id>7V zYhDRiyetIIR&Arjm7Yy4X_ci)RW=sQt3tMorE8&W3@q1LO4mC3;yMKvFY~U_*+%QF zy3S&wPF7WM!D1x9xQ21C0QjSZkOx6wte#JvR@UEEuUX4P^r3t3vYF=g9aZ={=% zj9ewUEmka%`7YzR&i_Vo3fBMb&wo)us`;+l7gg8H9|(1++wJ`8uNL3l+3xkGZSJh| zT6n{JNZje7y1U)mDQ|a8)#lv8;)OQKX4zKup|croE{KCebmPWR^^h)2LNAW=uHO|1|95P2bc{3&!iuj#C!g0MuXI0 z8JWaH2hyhyk(l(hiyr$-gFf*-BNKzq6zn52^*%?w!k`~RH?bL;Bk*zkC1zrd2OU;~ zxcv;=P-i3mT1ntxHry3lZ)~&?#d@PHEgEfu8ZcpWz2Y|by=`kdQr(5h>un`wCpM^x z=2G5(V0r2@s8wDGHXbncu-VW@+Pi-lJi zRAb5h=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "game2048" +version = "0.1.0" +requires-python = ">=3.8" + +[project.scripts] +play2048 = "game2048.main:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["test"] diff --git a/2048/AI.py b/2048/src/game2048/AI.py similarity index 91% rename from 2048/AI.py rename to 2048/src/game2048/AI.py index 5021cac..f89b9e2 100644 --- a/2048/AI.py +++ b/2048/src/game2048/AI.py @@ -3,7 +3,7 @@ import random import copy import math -from board import Board +from .board import Board class AI(object): def __init__(self, GameBoard, level): @@ -440,58 +440,3 @@ def playTheGame(self): if self.GameBoard.reaches2048(): return 1, self.GameBoard.score else: return 0, self.GameBoard.score -# test -if __name__ == "__main__": - # # novice AI play 100 times - # record = [] - # scores = [] - # for i in range(100): - # testBoard = Board(4) - # noviceAI = AI(testBoard, 0) - # res = noviceAI.playTheGame() - # record.append(res[0]) - # scores.append(res[1]) - # print("Novice AI:") - # print("record:", record) - # print("scores:", scores) - # avgscore = sum(scores)/len(scores) - # print("average score: ", avgscore) - # winrate = sum(record)/len(record) - # print("winrate: ", winrate) - - # competent AI play 20 times - record = [] - scores = [] - for i in range(20): - testBoard = Board(4) - competentAI = AI(testBoard, 2) - res = competentAI.playTheGame() - record.append(res[0]) - scores.append(res[1]) - print("Competent AI:") - print("record:", record) - print("scores:", scores) - avgscore = sum(scores)/len(scores) - print("average score: ", avgscore) - winrate = sum(record)/len(record) - print("winrate: ", winrate) - - # # proficient AI play 20 times - # record = [] - # scores = [] - # for i in range(20): - # testBoard = Board(4) - # competentAI = AI(testBoard, 2) - # res = competentAI.playTheGame() - # record.append(res[0]) - # scores.append(res[1]) - # print("Proficient AI:") - # print("record:", record) - # print("scores:", scores) - # avgscore = sum(scores)/len(scores) - # print("average score: ", avgscore) - # winrate = sum(record)/len(record) - # print("winrate: ", winrate) - - - diff --git a/2048/RL.py b/2048/src/game2048/RL.py similarity index 99% rename from 2048/RL.py rename to 2048/src/game2048/RL.py index 763f055..339d44a 100644 --- a/2048/RL.py +++ b/2048/src/game2048/RL.py @@ -10,7 +10,7 @@ import torch.nn.functional as F import numpy as np -from board import Board +from .board import Board # Reference: https://github.com/navjindervirdee/2048-deep-reinforcement-learning # for deep reinforcement learning: currently only consider board with size 4 (can be improved later) diff --git a/2048/UI.py b/2048/src/game2048/UI.py similarity index 72% rename from 2048/UI.py rename to 2048/src/game2048/UI.py index 9c1f23f..f2fdfc4 100644 --- a/2048/UI.py +++ b/2048/src/game2048/UI.py @@ -1,7 +1,7 @@ ## 2048 game user interface (UI), using tkinter from tkinter import * -from board import Board -from AI import AI +from .board import Board +from .AI import AI import copy import string @@ -43,8 +43,10 @@ def init(self, data): data.paused = False # time settings - data.timerDelay = 1000 - data.timeCounter = 0 + data.timerDelay = 50 # base clock tick (ms), drives the elapsed-time display + data.aiMoveIntervalMs = 200 # how often the AI actually makes a move + data.msSinceLastAIMove = 0 + data.elapsedMs = 0 # color settings data.colors = {0 : "#D7CCC8", @@ -70,14 +72,19 @@ def init(self, data): data.AIstep = 0 data.AI = None + # player settings + data.playerStep = 0 + def resetGameBoard(self, data): self.GameBoard = Board(data.size) data.board = self.GameBoard.board data.cellSize = (data.width - data.margin * 2) / data.size - data.timeCounter = 0 + data.elapsedMs = 0 + data.msSinceLastAIMove = 0 data.reach2048 = False data.continuePlaying = False data.cannotMove = False + data.playerStep = 0 def mousePressed(self, event, data): # three buttons in the start page @@ -100,12 +107,16 @@ def mousePressed(self, event, data): # customize size page elif data.customizeSizeMode: + layout = self.customizeSizeLayout(data) + sizeLeft, sizeTop, sizeRight, sizeBottom = layout["sizeBox"] + finishLeft, finishTop, finishRight, finishBottom = layout["finishBox"] + # press the empty "size button" to enter the board size - if data.width//2 <= event.x <= data.width*(3/4) and data.height//2 <= event.y <= data.height*(3/5): + if sizeLeft <= event.x <= sizeRight and sizeTop <= event.y <= sizeBottom: data.selectingBoardSize = True # press the finish button to enter the game state - elif data.width*(3/8) <= event.x <= data.width*(5/8) and data.height*(1/4+1/15) <= event.y <= data.height*(7/20+1/15): + elif finishLeft <= event.x <= finishRight and finishTop <= event.y <= finishBottom: self.resetGameBoard(data) if data.AImode: data.levelSelectionMode = True else: data.inGame = True @@ -134,6 +145,15 @@ def mousePressed(self, event, data): data.inGame = True def keyPressed(self, event, data): + # Esc: leave a paused game or a finished game (win or game over) and + # return to the home page + if event.keysym == "Escape" and ( + (data.inGame and data.paused) + or (not data.inGame and (data.reach2048 or data.cannotMove)) + ): + self.init(data) + return + # customize size mode if data.customizeSizeMode: if data.selectingBoardSize and event.keysym in string.digits: @@ -168,9 +188,10 @@ def keyPressed(self, event, data): elif direction == "Right": canMove = self.GameBoard.moveRight() # add a new number after each legal move - if canMove: - data.newTileIndex, data.newTileNum = self.GameBoard.addNewTile() + if canMove: + data.newTileIndex, data.newTileNum = self.GameBoard.addNewTile() print("data.newTileIndex: ", data.newTileIndex) + data.playerStep += 1 else: print("cannot move in this direction") self.GameBoard.printBoard() data.board = self.GameBoard.board @@ -216,40 +237,63 @@ def drawStartPage(self, canvas, data): canvas.create_rectangle(data.width*(5/6), data.height*(9/10), data.width*(29/30), data.height*(29/30), fill="light grey") canvas.create_text(data.width*(9/10), data.height*(14/15), text="Quit", font="Arial 30 bold") + # scale a font size off canvas width so text never overflows regardless of window size + def scaledFont(self, data, divisor): + return max(10, int(data.width / divisor)) + + # shared geometry for the customize-size page, used by both drawing and click handling + def customizeSizeLayout(self, data): + return { + "sizeBox": (data.width*(1/4), data.height*(2/5), data.width*(3/4), data.height*(1/2)), + "finishBox": (data.width*(3/8), data.height*(7/10), data.width*(5/8), data.height*(4/5)), + } + # customize size page def drawCustomizeSizePage(self, canvas, data): - canvas.create_rectangle(0, 0, data.width, data.height, fill="cyan") - canvas.create_text(data.width//2, data.height//4, text="Select Your Size Here!", font="Arial 50 bold", fill="purple") + canvas.create_rectangle(0, 0, data.width, data.height, fill="#ECEFF1") + canvas.create_text(data.width//2, data.height*(1/6), text="Choose Your Board Size", + font=("Arial", self.scaledFont(data, 16), "bold"), fill="#FDD835") - # sizes (4-10) - canvas.create_text(data.width//4, data.height*(11/20), text="Board Size(Width):", font="Arial 30") - if data.selectingBoardSize: - canvas.create_rectangle(data.width//2, data.height//2, data.width*(3/4), data.height*(3/5), fill="light goldenrod") - else: - canvas.create_rectangle(data.width//2, data.height//2, data.width*(3/4), data.height*(3/5), fill="lemon chiffon") - canvas.create_text(data.width*(5/8), data.height*(11/20), text=data.size, font="Arial 30") - canvas.create_text(data.width*(7/8), data.height*(11/20), text="(4-10)", font="Arial 30") + layout = self.customizeSizeLayout(data) + sizeLeft, sizeTop, sizeRight, sizeBottom = layout["sizeBox"] + finishLeft, finishTop, finishRight, finishBottom = layout["finishBox"] + sizeCenterX, sizeCenterY = (sizeLeft+sizeRight)/2, (sizeTop+sizeBottom)/2 + finishCenterX, finishCenterY = (finishLeft+finishRight)/2, (finishTop+finishBottom)/2 - # finish button - canvas.create_rectangle(data.width*(3/8), data.height*(1/4+1/15), data.width*(5/8), data.height*(7/20+1/15), fill="lemon chiffon") - canvas.create_text(data.width//2, data.height*(3/10+1/15), text="Finish!", font="Arial 35") + # label above the size box + canvas.create_text(sizeCenterX, sizeTop - data.height*0.06, text="Board Size (Width)", + font=("Arial", self.scaledFont(data, 32)), fill="#546E7A") - # AI mode level selection page - def drawLevelSelectionPage(self, canvas, data): - canvas.create_rectangle(0, 0, data.width, data.height, fill="cyan") - canvas.create_text(data.width//2, data.height//4, text="Select a Level!", font="Arial 55 bold", fill="purple") + # size box itself, highlighted while the player is entering a number + boxFill = "light goldenrod" if data.selectingBoardSize else "lemon chiffon" + canvas.create_rectangle(sizeLeft, sizeTop, sizeRight, sizeBottom, fill=boxFill, outline="#795548", width=2) + canvas.create_text(sizeCenterX, sizeCenterY, text=str(data.size), + font=("Arial", self.scaledFont(data, 12), "bold"), fill="#795548") - # novice mode - canvas.create_rectangle(data.width//4, data.height//2, data.width*(3/4), data.height*(3/5), fill="lemon chiffon") - canvas.create_text(data.width//2, data.height*(11/20), text="Novice", font="Arial 35 bold") + # hint below the size box + canvas.create_text(sizeCenterX, sizeBottom + data.height*0.04, text="press a number key: 4-9, or 1 for 10", + font=("Arial", self.scaledFont(data, 34)), fill="#78909C") - # competent mode - canvas.create_rectangle(data.width//4, data.height*(3/5+1/30), data.width*(3/4), data.height*(7/10+1/30), fill="lemon chiffon") - canvas.create_text(data.width//2, data.height*(13/20+1/30), text="Competent", font="Arial 35 bold") + # finish button + canvas.create_rectangle(finishLeft, finishTop, finishRight, finishBottom, fill="lemon chiffon", outline="#795548", width=2) + canvas.create_text(finishCenterX, finishCenterY, text="Finish!", + font=("Arial", self.scaledFont(data, 20), "bold")) - # expert mode - canvas.create_rectangle(data.width//4, data.height*(7/10+1/15), data.width*(3/4), data.height*(4/5+1/15), fill="lemon chiffon") - canvas.create_text(data.width//2, data.height*(3/4+1/15), text="Expert", font="Arial 35 bold") + # AI mode level selection page + def drawLevelSelectionPage(self, canvas, data): + canvas.create_rectangle(0, 0, data.width, data.height, fill="#ECEFF1") + canvas.create_text(data.width//2, data.height*(1/6), text="Select a Level!", + font=("Arial", self.scaledFont(data, 15), "bold"), fill="#FDD835") + + # (label, box top, box bottom, label y) - same regions used by mousePressed + levels = [ + ("Novice", data.height//2, data.height*(3/5), data.height*(11/20)), + ("Competent", data.height*(3/5+1/30), data.height*(7/10+1/30), data.height*(13/20+1/30)), + ("Expert", data.height*(7/10+1/15), data.height*(4/5+1/15), data.height*(3/4+1/15)), + ] + for label, top, bottom, labelY in levels: + canvas.create_rectangle(data.width//4, top, data.width*(3/4), bottom, fill="lemon chiffon", outline="#795548", width=2) + canvas.create_text(data.width//2, labelY, text=label, font=("Arial", self.scaledFont(data, 20), "bold")) # game page def drawCell(self, canvas, data, row, col): @@ -273,16 +317,22 @@ def drawBoard(self, canvas, data): canvas.create_text(data.margin+data.cellSize/2+col*data.cellSize, data.titlePlace+data.margin+data.cellSize/2+row*data.cellSize, text=data.board[row][col], font = ("Arial", textSize), fill="black") + def formatElapsedTime(self, elapsedMs): + totalSeconds, ms = divmod(elapsedMs, 1000) + minutes, seconds = divmod(totalSeconds, 60) + return "%02d:%02d.%03d" % (minutes, seconds, ms) + def drawGamePage(self, canvas, data): canvas.create_rectangle(0, 0, data.width, data.height, fill="#EFEBE9") canvas.create_text(data.width//4, data.titlePlace//2, text="2048", font="Arial 60 bold", fill="#795548") if data.reach2048: canvas.create_text(data.width//4, data.titlePlace*(3/4), text="Reached 2048", font="TimesNewRoman 30 bold", fill="red") + canvas.create_text(data.width*(3/4), data.titlePlace*(1/4), text="Score:"+str(self.GameBoard.score), font="Arial 23 bold", fill="purple") if data.AImode: # AI mode - canvas.create_text(data.width//2, data.titlePlace//2, text="Step:"+str(data.AIstep), font="Arial 23 bold", fill="purple") + canvas.create_text(data.width*(3/4), data.titlePlace*(2/4), text="Step:"+str(data.AIstep), font="Arial 23 bold", fill="purple") else: # player mode - canvas.create_text(data.width//2, data.titlePlace//2, text= "Time:"+str(data.timeCounter), font="Arial 23 bold", fill="purple") - canvas.create_text(data.width*(3/4), data.titlePlace//2, text="Score:"+str(self.GameBoard.score), font="Arial 23 bold", fill="purple") + canvas.create_text(data.width*(3/4), data.titlePlace*(2/4), text="Step:"+str(data.playerStep), font="Arial 23 bold", fill="purple") + canvas.create_text(data.width*(3/4), data.titlePlace*(3/4), text="Time:"+self.formatElapsedTime(data.elapsedMs), font="Arial 23 bold", fill="purple") self.drawBoard(canvas, data) # Game paused @@ -340,17 +390,21 @@ def AImove(self, data): def timerFired(self, data): if data.inGame and not (data.reach2048 or data.cannotMove) and not data.paused: - data.timeCounter += 1 + # data.timerDelay is a fast, constant clock tick (in both modes) so the + # elapsed-time display can show smooth sub-second precision; the AI's + # actual move rate is throttled separately via aiMoveIntervalMs + data.elapsedMs += data.timerDelay if data.AImode: - data.timerDelay = 200 - if not self.GameBoard.GameOver(): - # move twice in each timer delay period - self.AImove(data) - else: - data.inGame = False - if self.GameBoard.reaches2048(): data.reach2048 = True - else: data.cannotMove = True + data.msSinceLastAIMove += data.timerDelay + if data.msSinceLastAIMove >= data.aiMoveIntervalMs: + data.msSinceLastAIMove = 0 + if not self.GameBoard.GameOver(): + self.AImove(data) + else: + data.inGame = False + if self.GameBoard.reaches2048(): data.reach2048 = True + else: data.cannotMove = True def runGame(self, width, height): # tkinter starter code def redrawAllWrapper(canvas, data): diff --git a/2048/src/game2048/__init__.py b/2048/src/game2048/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/2048/board.py b/2048/src/game2048/board.py similarity index 80% rename from 2048/board.py rename to 2048/src/game2048/board.py index 79b04bd..4ade788 100644 --- a/2048/board.py +++ b/2048/src/game2048/board.py @@ -205,72 +205,3 @@ def move(self): print("Game Over!\n") break print("reach here after game is over!") - - -# test -if __name__ == "__main__": - board = Board(4) - board.printBoard() - board.addNewTile() - board.printBoard() - board.addNewTile() - board.addNewTile() - board.addNewTile() - board.addNewTile() - board.addNewTile() - board.printBoard() - print("after moving left:") - board.moveLeft() - board.printBoard() - - board1 = copy.deepcopy(board) - print("-----------after transpose:") - board1.transpose() - board1.printBoard() - print("after mirror:") - board1.mirror() - board1.printBoard() - - print("------------after rotation:") - board.rotate90Clockwise() - board.printBoard() - - print("------------rotate back:") - board.rotate90CounterClockwise() - board.printBoard() - - print("after moving down:") - board.moveDown() - board.printBoard() - - print("after moving up:") - board.moveUp() - board.printBoard() - - print("after moving right:") - board.moveRight() - board.printBoard() - - print("game over tests:") - # test with infinite random moves - board = Board(6) - while not board.GameOver(): - canMove = False - direction = random.choice(board.directionList) - print("direction:", direction) - if direction == "Up": - canMoveUp = board.moveUp() - canMove = canMoveUp - elif direction == "Down": - canMoveDown = board.moveDown() - canMove = canMoveDown - elif direction == "Left": - canMoveLeft = board.moveLeft() - canMove = canMoveLeft - elif direction == "Right": - canMoveRight = board.moveRight() - canMove = canMoveRight - # add a new number after each legal move - if canMove: board.addNewTile() - else: print("cannot move in this direction") - board.printBoard() diff --git a/2048/2048.py b/2048/src/game2048/main.py similarity index 72% rename from 2048/2048.py rename to 2048/src/game2048/main.py index fb9dcf1..3189095 100644 --- a/2048/2048.py +++ b/2048/src/game2048/main.py @@ -1,5 +1,5 @@ ## 2048 game -from UI import UI +from .UI import UI class Game(object): def __init__(self): @@ -8,5 +8,8 @@ def __init__(self): def run2048Game(self): self.GameUI.runGame(600, 750) +def main(): + Game().run2048Game() + if __name__ == "__main__": - Game().run2048Game() \ No newline at end of file + main() \ No newline at end of file diff --git a/2048/test/test_ai.py b/2048/test/test_ai.py new file mode 100644 index 0000000..004754e --- /dev/null +++ b/2048/test/test_ai.py @@ -0,0 +1,114 @@ +## pytest tests for AI.py + +import pytest + +from game2048.board import Board +from game2048.AI import AI + + +def make_board(rows, score=0): + """Build a Board with a fixed size and layout, bypassing the random init tile.""" + size = len(rows) + board = Board(size) + board.board = [row[:] for row in rows] + board.score = score + return board + + +def make_ai(rows, level=2, score=0): + board = make_board(rows, score=score) + return AI(board, level) + + +def test_evaluate_uses_weight_board(): + ai = make_ai([ + [2, 0], + [0, 0], + ]) + assert ai.evaluate() == 2 * ai.weightBoard[0][0] + + +def test_evaluate_sums_weighted_tiles(): + ai = make_ai([ + [2, 4], + [0, 8], + ]) + expected = ( + 2 * ai.weightBoard[0][0] + + 4 * ai.weightBoard[0][1] + + 8 * ai.weightBoard[1][1] + ) + assert ai.evaluate() == expected + + +def test_get_legal_moves_excludes_blocked_directions(): + # only row 0 is filled with a mergeable pair: Up is blocked (already at + # the top), Down/Left/Right all change the board. + ai = make_ai([ + [2, 2, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + legalMoves = ai.getLegalMoves() + assert sorted(legalMoves) == [1, 2, 3] # Down, Left, Right + + +def test_get_legal_moves_empty_when_board_is_stuck(): + ai = make_ai([ + [2, 4, 2, 4], + [4, 2, 4, 2], + [2, 4, 2, 4], + [4, 2, 4, 2], + ]) + assert ai.getLegalMoves() == [] + + +def test_perform_action_applies_move_and_adds_new_tile(): + ai = make_ai([ + [2, 2, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + ai.performAction(2) # Left + + # the merge always lands in the leftmost column; the newly added tile + # can land on any empty cell, so only check what performAction guarantees. + assert ai.GameBoard.board[0][0] == 4 + assert ai.GameBoard.score == 4 + nonEmpty = sum(1 for row in ai.GameBoard.board for x in row if x != 0) + assert nonEmpty == 2 # merged tile + the newly added tile + + +def test_add_new_num_places_requested_value(): + ai = make_ai([ + [0, 0], + [0, 0], + ]) + ai.addNewNum(((1, 1), 4)) + assert ai.GameBoard.board[1][1] == 4 + + +def test_add_new_num_rejects_non_empty_tile(): + ai = make_ai([ + [2, 0], + [0, 0], + ]) + with pytest.raises(AssertionError): + ai.addNewNum(((0, 0), 4)) + + +def test_get_important_tiles_returns_highest_weight_empty_tiles(): + ai = make_ai([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + importantTiles = ai.getImporantTiles(3) + allTiles = [(i, j) for i in range(4) for j in range(4)] + expected = sorted( + allTiles, key=lambda coord: ai.weightBoard[coord[0]][coord[1]], reverse=True + )[:3] + assert importantTiles == expected diff --git a/2048/test/test_board.py b/2048/test/test_board.py new file mode 100644 index 0000000..b6e83a0 --- /dev/null +++ b/2048/test/test_board.py @@ -0,0 +1,227 @@ +## pytest tests for board.py + +import copy +import pytest + +from game2048.board import Board + + +def make_board(rows, score=0): + """Build a Board with a fixed size and layout, bypassing the random init tile.""" + size = len(rows) + board = Board(size) + board.board = [row[:] for row in rows] + board.score = score + return board + + +def test_init_board_has_exactly_one_starting_tile(): + board = Board(4) + assert board.size == 4 + assert len(board.board) == 4 + assert all(len(row) == 4 for row in board.board) + + nonEmpty = [(i, j) for i in range(4) for j in range(4) if board.board[i][j] != 0] + assert len(nonEmpty) == 1 + i, j = nonEmpty[0] + assert board.board[i][j] in (2, 4) + assert board.score == 0 + + +def test_add_new_tile_fills_an_empty_cell(): + board = make_board([ + [2, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + addIndex, addNum = board.addNewTile() + + assert addNum in (2, 4) + assert board.board[addIndex[0]][addIndex[1]] == addNum + nonEmpty = sum(1 for row in board.board for x in row if x != 0) + assert nonEmpty == 2 + + +def test_is_same_board(): + board = Board(4) + b1 = [[1, 2], [3, 4]] + b2 = [[1, 2], [3, 4]] + b3 = [[1, 2], [3, 5]] + assert board.isSameBoard(b1, b2) is True + assert board.isSameBoard(b1, b3) is False + + +def test_move_left_merges_and_shifts(): + board = make_board([ + [2, 2, 0, 4], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + canMove = board.moveLeft() + assert canMove is True + assert board.board[0] == [4, 4, 0, 0] + assert board.score == 4 + + +def test_move_left_merges_only_once_per_pair(): + board = make_board([ + [2, 2, 2, 2], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + board.moveLeft() + assert board.board[0] == [4, 4, 0, 0] + assert board.score == 8 + + +def test_move_left_returns_false_when_no_move_possible(): + board = make_board([ + [2, 4, 8, 16], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + canMove = board.moveLeft() + assert canMove is False + assert board.board[0] == [2, 4, 8, 16] + + +def test_transpose(): + board = make_board([ + [1, 2], + [3, 4], + ]) + board.transpose() + assert board.board == [[1, 3], [2, 4]] + + +def test_mirror(): + board = make_board([ + [1, 2], + [3, 4], + ]) + board.mirror() + assert board.board == [[2, 1], [4, 3]] + + +def test_rotate90_clockwise(): + board = make_board([ + [1, 2], + [3, 4], + ]) + board.rotate90Clockwise() + assert board.board == [[3, 1], [4, 2]] + + +def test_rotate180(): + board = make_board([ + [1, 2], + [3, 4], + ]) + board.rotate180() + assert board.board == [[4, 3], [2, 1]] + + +def test_rotate90_counterclockwise(): + board = make_board([ + [1, 2], + [3, 4], + ]) + board.rotate90CounterClockwise() + assert board.board == [[2, 4], [1, 3]] + + +def test_rotate90_clockwise_then_counterclockwise_is_identity(): + original = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + ] + board = make_board(original) + board.rotate90Clockwise() + board.rotate90CounterClockwise() + assert board.board == original + + +def test_move_down_merges_column(): + board = make_board([ + [2, 0, 0, 0], + [2, 0, 0, 0], + [0, 0, 0, 0], + [4, 0, 0, 0], + ]) + canMove = board.moveDown() + assert canMove is True + expectedCol0 = [0, 0, 4, 4] + assert [board.board[i][0] for i in range(4)] == expectedCol0 + assert board.score == 4 + + +def test_move_up_merges_column(): + board = make_board([ + [2, 0, 0, 0], + [0, 0, 0, 0], + [2, 0, 0, 0], + [4, 0, 0, 0], + ]) + canMove = board.moveUp() + assert canMove is True + expectedCol0 = [4, 4, 0, 0] + assert [board.board[i][0] for i in range(4)] == expectedCol0 + assert board.score == 4 + + +def test_move_right_merges_and_shifts(): + board = make_board([ + [4, 0, 2, 2], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]) + canMove = board.moveRight() + assert canMove is True + assert board.board[0] == [0, 0, 4, 4] + assert board.score == 4 + + +def test_reaches2048_false_when_below_threshold(): + board = make_board([ + [1024, 0], + [0, 0], + ]) + assert board.reaches2048() is False + + +def test_reaches2048_true_when_tile_at_or_above_threshold(): + board = make_board([ + [2048, 0], + [0, 0], + ]) + assert board.reaches2048() is True + + +def test_game_over_false_when_a_move_is_available(): + board = make_board([ + [2, 4, 8, 16], + [4, 8, 16, 32], + [8, 16, 32, 64], + [16, 32, 2, 2], + ]) + assert board.GameOver() is False + # GameOver should restore the board and score after probing moves + assert board.board[3][2:] == [2, 2] + assert board.score == 0 + + +def test_game_over_true_when_no_moves_and_no_merges_possible(): + board = make_board([ + [2, 4, 2, 4], + [4, 2, 4, 2], + [2, 4, 2, 4], + [4, 2, 4, 2], + ]) + assert board.GameOver() is True diff --git a/README.md b/README.md index 4091ae7..94ed265 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,63 @@ 2048 game implemented by python3 and tkinter AI part: Expectiminimax with alpha-beta pruning (Normal level), Reinforcement Learning (Hard level) + +## Requirements + +- Python 3.8+ +- tkinter (bundled with the standard Windows/macOS Python installers; on Linux you may need to install it separately, e.g. `sudo apt install python3-tk`) + +## Installation + +The game lives in the `2048/` folder, which is a self-contained Python package (`game2048`, `src/` layout). Install it in editable mode: + +```bash +cd 2048 +pip install -e . +``` + +This installs the `game2048` package plus a `play2048` console command. + +## Running the game + +After installing, either of these will launch the game window: + +```bash +play2048 +``` + +```bash +python -m game2048.main +``` + +### Controls + +- **Arrow keys** — move tiles (Player mode) +- **p** — pause / unpause +- **Esc** — while paused, or after winning/losing, return to the home page +- **r** — restart (once the game is over) +- **c** — keep playing after reaching 2048 + +## Running the tests + +From the `2048/` folder (after installing the package as above): + +```bash +pytest +``` + +## Project structure + +``` +2048/ + pyproject.toml # package metadata, editable-install config, pytest config + src/game2048/ + board.py # board state and move/merge logic + AI.py # Expectiminimax / alpha-beta AI opponent + RL.py # reinforcement-learning experiments (not wired into the UI yet) + UI.py # tkinter UI and game loop + main.py # entry point (play2048 / python -m game2048.main) + test/ + test_board.py + test_ai.py +``` From 14a14188e4c1bca3d6aeccba2c754d30e441d40d Mon Sep 17 00:00:00 2001 From: Yilun Wu Date: Mon, 6 Jul 2026 20:00:43 -0400 Subject: [PATCH 2/2] fix CI issue in develop --- .github/workflows/workflow.yml | 28 ++++++++++++++++++++++++++++ 2048/.coverage | Bin 0 -> 53248 bytes 2048/pyproject.toml | 12 ++++++++++++ Makefile | 16 ++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 .github/workflows/workflow.yml create mode 100644 2048/.coverage create mode 100644 Makefile diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..3b4b82c --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: make install + + - name: Testing + run: make test + + - name: Refactoring + run: make refactor diff --git a/2048/.coverage b/2048/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..a30cbeb237641c30f82c5be4a569e3bf229a7722 GIT binary patch literal 53248 zcmeI)+iw(A90%~ZY zc6Oj2o;Ys#Qp`DS#q`DNN{^zd${`^XMM=}En_lsfqLoCvLBG|e^>(XiW&F&0-Nr{s zCV5sdKJI?S7|YCOe($=T*_%GyWu->b<8%NE1Rwwb2y7XF`iV>`*Wa&Rx#XLLvh-cE zDBbYAd+Ox$|KTWhpGXWED-{vgd`rAmh|E zPnP(aMf>Qg#R{;OI~rA)nWKV=j$7jB3$AGw z7o;~T%#|7z9g1_w4{Pf>mEFE0%9b5e)AlUia%>@&WU=PU(wY?eBPP?4#^7>9x z*>$(g8g1FAZ%cE=Qa7&4q&gpEVJxp~a&jxkJ^2byrAgcRvB?QI+^#UKi0r_kEhf*#{Z8Xz_Qn~Hh)p|H0cwXXHM@uDmPh$wIx)7{C{Hp0Mh=*h6-4ysxM>4m6d%M6ng!}}* zC$IKsLC9a|fqsQ1eSYaJA@iO_$j9XJ*hRBUvq{yoTpF@82ZvVx!3j$yB{mCA%@6u7 zZwB3yuRV=;!vY%Hu|1_436j(B{=o4(i!Q5t+4AG3S&4T*x|l%;8h*9OWl zM(=1INk(xUfgEtxSW_Sjk5yNILgmiJL z=mS#c2;bnV#y<-EV1WPxAOHafKmY;|fB*y_009U}X@V~@?#A5{o;%zAEdmbdum?CBZ$0{~MUn=9nxaVlgwL4_C_kECK+ zHpilGE|y8I&=HLa>D_oN1;=ztRL+b_<>WSAPCjqhmY>g4QT+TrnWewYHqisy-c7_3-~VefiFz)*aowi-fBgMFj8+Ih00Izz00bZa0SG_<0uX?} zmKM-eUFqiM|Elq?LO)m_009U<00Izz00bZa0SG_<0ub1O0=nLv=I8&q@wH<7Wc)zC zus{F;5P$##AOHafKmY;|fB*y_@YDr%=m|BwPy6}6H@ENKyL8~c9jP1lZ~S%XmoHzs zb@OIlhn7gEv)X=@5-sUVgFjwYl$RgWZ+-ghQH`GJA5;}3@gSinJwe>G6|5Zkj1L5> zy*&wj{;wN%72^-%uJQL%uOLnc0SG_<0uX=z1Rwwb2tWV=5P-nr7D&<;1*)cZ(ANg4 z)B0e=3E`~T;pF%Sj>AOHaf vKmY;|fB*y_009U<;0X!f`TrAwMX?Zo00bZa0SG_<0uX=z1Rwx`=Sbjx?c8w_ literal 0 HcmV?d00001 diff --git a/2048/pyproject.toml b/2048/pyproject.toml index d61fe80..a254627 100644 --- a/2048/pyproject.toml +++ b/2048/pyproject.toml @@ -6,6 +6,18 @@ build-backend = "setuptools.build_meta" name = "game2048" version = "0.1.0" requires-python = ">=3.8" +dependencies = [ + "numpy", + "torch", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "black", + "pylint", +] [project.scripts] play2048 = "game2048.main:main" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..00c2cd6 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +install: + pip install --upgrade pip + pip install -e "./2048[dev]" + +test: + cd 2048 && python -m pytest -vv --cov=game2048 test + +format: + black 2048/src 2048/test + +lint: + pylint --disable=R,C,pointless-string-statement,undefined-variable,no-member,function-redefined,unused-variable,unused-import,redefined-outer-name --ignore=UI.py --ignore-patterns=test_.*?py 2048/src/game2048 + +refactor: format lint + +all: install test refactor