Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
@@ -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
Binary file added 2048/.coverage
Binary file not shown.
4 changes: 4 additions & 0 deletions 2048/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.pytest_cache/
*.egg-info/
Binary file removed 2048/__pycache__/AI.cpython-36.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/AI.cpython-38.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/AI.cpython-39.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/UI.cpython-36.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/UI.cpython-38.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/UI.cpython-39.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/board.cpython-36.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/board.cpython-38.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/board.cpython-39.pyc
Binary file not shown.
Binary file removed 2048/__pycache__/image_util.cpython-36.pyc
Binary file not shown.
29 changes: 29 additions & 0 deletions 2048/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
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"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["test"]
57 changes: 1 addition & 56 deletions 2048/AI.py → 2048/src/game2048/AI.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)



2 changes: 1 addition & 1 deletion 2048/RL.py → 2048/src/game2048/RL.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
148 changes: 101 additions & 47 deletions 2048/UI.py → 2048/src/game2048/UI.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Empty file added 2048/src/game2048/__init__.py
Empty file.
Loading
Loading