diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fb9ddb --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/build + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7d7e588 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM nginx:latest + +RUN apt-get update && apt-get install -y nodejs npm + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +WORKDIR /usr/share/nginx/html +COPY index.html . + +RUN mkdir -p /usr/share/nginx/html/public +COPY src/*.css ./public + +RUN cp /app/build/* ./public + +EXPOSE 80 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..29e4f74 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,23 @@ +pipeline { + agent any + stages { + stage('Checkout') { + steps { + checkout scm + } + } + stage('Build') { + steps { + script { + docker.build('test.trianta.dev:latest') + } + } + } + stage('Deploy') { + steps { + sh 'docker stop test && docker rm test || exit 0' + sh 'docker run -d -p 3466:80 --name test test.trianta.dev:latest' + } + } + } +} diff --git a/index.html b/index.html index e7b82cb..0eb4389 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,10 @@ Trianta - + - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..255b297 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "trianta.dev", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trianta.dev", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "typescript": "^5.5.4" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..51f85b2 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "trianta.dev", + "version": "0.1.0", + "description": "My personal website", + "main": "index.html", + "scripts": { + "build": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://lab.trianta.dev/Trianta/trianta.dev.git" + }, + "author": "trianta", + "license": "MIT", + "dependencies": { + "typescript": "^5.5.4" + } +} diff --git a/css/home.css b/src/home.css similarity index 96% rename from css/home.css rename to src/home.css index 751f8bb..90107a7 100644 --- a/css/home.css +++ b/src/home.css @@ -125,8 +125,11 @@ a.button { border-top-left-radius: 18px; border-top-right-radius: 18px; animation: card 3s linear infinite; - max-width: 800px; + width: 800px; + height: 800px; margin: 0px auto; + margin-top: 25px; + margin-bottom: 25px; } .cardTop { diff --git a/css/pong.css b/src/pong.css similarity index 100% rename from css/pong.css rename to src/pong.css diff --git a/js/pong.js b/src/pong.js similarity index 100% rename from js/pong.js rename to src/pong.js diff --git a/src/snake.ts b/src/snake.ts new file mode 100644 index 0000000..529befe --- /dev/null +++ b/src/snake.ts @@ -0,0 +1,286 @@ +enum BoardState { + SNAKE = 0, + FOOD = 1, + CHECKED = 2 // For search algorithms +} + +// Bit functions for ease +function isBitSet(value: number, bit: number): boolean { + return ((value >> bit) % 2 != 0) +} + +function bitSet(value: number, bit: number): number { + return value | 1 << bit; +} + +function bitClear(value: number, bit: number): number { + return value & ~(1 << bit); +} + +class Point { + x: number; + y: number; + + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + + copy(other: Point) { + this.x = other.x; + this.y = other.y; + } + + add(other: Point) { + var result = new Point; + result.x = this.x + other.x; + result.y = this.y + other.y; + return result; + } + + subtract(other: Point) { + var result = new Point; + result.x = this.x - other.x; + result.y = this.y - other.y; + return result; + } +} + +class SnakeCore { + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + grid: number; + timeout: number; + width: number; + height: number; + board: number[][]; + body: Point[]; + gameover: boolean; + foodAte: boolean; + + constructor() { + // TODO: Add CSS stuff for page + this.canvas = document.getElementById('snake') as HTMLCanvasElement; + this.context = this.canvas.getContext('2d') as CanvasRenderingContext2D; + this.grid = 25; // size of grid squares + this.timeout = 100; // speed in ms + this.width = 25; + this.height = 15; + this.board = []; + for (let i = 0; i < this.height; i++) + this.board.push(new Array(this.width)); + this.body = []; + this.gameover = false; + this.foodAte = true; + this.body.push(new Point(12, 8)); + this.board[8][12] = 1; + } + + get head(): Point { + return this.body[this.body.length - 1]; + } + + reset() { + console.debug("[TRACE] Reset was triggered"); + this.gameover = false; + this.foodAte = true; + for (let i = 0; i < this.height; i++) { + for (let j = 0; j < this.width; j++) { + this.board[i][j] = 0; + } + } + while (this.body.length > 0) + this.body.pop(); + while (g_snakebot.path.length > 0) + g_snakebot.path.pop(); + this.startRandom(); + this.foodRegen(); + this.draw(); + } + + startRandom() { + this.body.push(new Point(Math.floor(Math.random() * this.width), Math.floor(Math.random() * this.height))); + } + + foodRegen() { + if (!this.foodAte) + return; + console.debug("[TRACE] Food reset was triggered"); + this.foodAte = false; + while (true) { + let tmp = new Point(Math.floor(Math.random() * this.width), Math.floor(Math.random() * this.height)); + if (isBitSet(this.board[tmp.y][tmp.x], BoardState.SNAKE)) + continue; + this.board[tmp.y][tmp.x] = bitSet(this.board[tmp.y][tmp.x], BoardState.FOOD); + return; + } + } + + // Simulate game logic + simulate() { + // Move snake + let next: Point = new Point; + next.copy(g_snakebot.nextMove()); + if (next.x < 0 || next.x > g_snake.width || next.y < 0 || next.y > g_snake.height) { + g_snake.gameover = true; + return; + } + if (isBitSet(g_snake.board[next.y][next.x], BoardState.SNAKE) && (g_snake.body.length > 1)) { + g_snake.gameover = true; // Game should end (Snake touching snake) + return; + } + + g_snake.board[next.y][next.x] = bitSet(g_snake.board[next.y][next.x], BoardState.SNAKE); + g_snake.body.push(next); + if (!isBitSet(g_snake.board[next.y][next.x], BoardState.FOOD)) { + let old: Point = g_snake.body.shift() as Point; + g_snake.board[old.y][old.x] = bitClear(g_snake.board[old.y][old.x], BoardState.SNAKE); + } else { + g_snake.board[next.y][next.x] = bitClear(g_snake.board[next.y][next.x], BoardState.FOOD); + g_snake.foodAte = true; + while (g_snakebot.path.length > 0) + g_snakebot.path.pop(); + } + } + + // Draw game to canvas + draw() { + // Clear the screen + g_snake.context.clearRect(0, 0, g_snake.canvas.width, g_snake.canvas.height); + + // Draw game + for (let i = 0; i < g_snake.height; i++) { + for (let j = 0; j < g_snake.width; j++) { + if (isBitSet(g_snake.board[i][j], BoardState.SNAKE)) + g_snake.context.fillStyle = "green"; + else if (isBitSet(g_snake.board[i][j], BoardState.FOOD)) + g_snake.context.fillStyle = "red"; + else + g_snake.context.fillStyle = "black"; + g_snake.context.fillRect(j * g_snake.grid, i * g_snake.grid, g_snake.grid, g_snake.grid); + } + } + } +} + +class Bot { + pathUntrimmed: Point[]; + path: Point[]; + + constructor() { + this.pathUntrimmed = []; + this.path = []; + } + + bfs() { + var search: Point[] = [g_snake.head]; + while (search.length !== 0) { + let current: Point = search.shift() as Point; + if (isBitSet(g_snake.board[current.y][current.x], BoardState.CHECKED)) + continue; + this.pathUntrimmed.push(current); + let locals: Point[] = new Array(4); + for (var i = 0; i < 4; i++) { + locals[i] = new Point; + locals[i].copy(current); + } + locals[0].y += 1; + locals[1].x += 1; + locals[2].y -= 1; + locals[3].x -= 1; + for (const local of locals) { + if (local.x < 0 || local.x > g_snake.width - 1 || local.y < 0 || local.y > g_snake.height - 1) + continue; + let value = g_snake.board[local.y][local.x]; + if (isBitSet(value, BoardState.FOOD)) { + this.pathUntrimmed.push(local); + return; + } + if (isBitSet(value, BoardState.CHECKED) || isBitSet(value, BoardState.SNAKE)) + continue; + search.push(local); + } + g_snake.board[current.y][current.x] = bitSet(g_snake.board[current.y][current.x], BoardState.CHECKED); + } + } + + // TODO: Fix trim function + trim() { + let reachedSnake = false; + this.path.push(this.pathUntrimmed.pop() as Point); // Push food location + while (this.pathUntrimmed.length !== 0) { + let location: Point = this.pathUntrimmed.pop(); + if (reachedSnake) + continue; + if (isBitSet(g_snake.board[location.y][location.x], BoardState.SNAKE)) { + reachedSnake = true; + continue; + } + var delta = new Point; + delta = location.subtract(this.path[this.path.length - 1]); + if ((Math.abs(delta.x) + Math.abs(delta.y)) === 1) + this.path.push(location); + } + } + + unvisit() { + for (let i = 0; i < g_snake.height; i++) { + for (let j = 0; j < g_snake.width; j++) { + g_snake.board[i][j] = bitClear(g_snake.board[i][j], BoardState.CHECKED); + } + } + } + + /* + values: + 0 = left + 1 = up + 2 = right + 4 = down + */ + nextMove() { + // Get new path to food + if (this.path.length === 0) + this.pathRefresh(); + var next: Point = new Point; + next.copy(this.path.pop()); + var delta = new Point; + delta = next.subtract(g_snake.head); + if (delta.x > 1) + console.log("[ERR] delta.x > 1"); + else if (delta.x < -1) + console.log("[ERR] delta.x < 1"); + if (delta.y > 1) + console.log("[ERR] delta.y > 1"); + else if (delta.y < -1) + console.log("[ERR] delta.y < 1"); + return next; + } + + pathRefresh() { + this.bfs(); + this.trim(); + this.unvisit(); + } +} + +const g_snake: SnakeCore = new SnakeCore(); +const g_snakebot: Bot = new Bot(); + +// game loop +function snakeloop() { + // Reset of needed + if (g_snake.gameover) + g_snake.reset(); + + // Simulate movement of snake + g_snake.simulate(); + + // Regenerate food if needed + g_snake.foodRegen(); + + g_snake.draw(); +} + +// start the game +setInterval(snakeloop, g_snake.timeout); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fe73056 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "outDir": "./build", + "allowJs": true, + "target": "es5" + }, + "include": ["./src/**/*"] +}