Compare commits

...

4 commits

Author SHA1 Message Date
9107c557af
Store game in localStorage
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-15 01:02:20 +01:00
e34ae6d1c3
Roll back to single workspace environment 2022-12-15 00:57:54 +01:00
6df42d2e3f
Remove express server 2022-12-15 00:53:33 +01:00
d87da24d30
Replace template fields in license 2022-12-15 00:49:30 +01:00
35 changed files with 99 additions and 862 deletions

View file

@ -7,11 +7,14 @@ module.exports = {
`plugin:${PluginName}/${PresetName.RecommendedWithTypeChecking}`
],
env: {
node: true
node: true,
browser: true
},
parserOptions: {
project: [
join(__dirname, "eslint.jsconfig.json")
join(__dirname, "app.jsconfig.json"),
join(__dirname, "eslint.jsconfig.json"),
join(__dirname, "gulp.tsconfig.json")
]
}
};

View file

@ -1,15 +1,6 @@
{
"folders": [
{
"name": "ConnectForce",
"path": "./packages/game"
},
{
"name": "Server",
"path": "./packages/server"
},
{
"name": "Solution Items",
"path": "."
}
],
@ -35,7 +26,7 @@
"label": "Build",
"type": "shell",
"options": {
"cwd": "${workspaceFolder:Solution Items}"
"cwd": "${workspaceFolder}"
},
"command": "npm",
"args": [
@ -72,7 +63,7 @@
"label": "Rebuild",
"type": "shell",
"options": {
"cwd": "${workspaceFolder:Solution Items}"
"cwd": "${workspaceFolder}"
},
"command": "npm",
"args": [
@ -88,7 +79,7 @@
"label": "Lint",
"type": "shell",
"options": {
"cwd": "${workspaceFolder:Solution Items}"
"cwd": "${workspaceFolder}"
},
"command": "npm",
"args": [
@ -113,33 +104,11 @@
"request": "launch",
"name": "Launch Website in Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder:ConnectForce}/lib/static",
"webRoot": "${workspaceFolder}/lib/static",
"preLaunchTask": "Build",
"pathMapping": {
"/": "${workspaceFolder:ConnectForce}/src"
},
"presentation": {
"hidden": true
"/": "${workspaceFolder}/src"
}
},
{
"type": "node",
"request": "launch",
"name": "Launch Server",
"program": "${workspaceFolder:Server}/src/main.js",
"console": "integratedTerminal",
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Launch Project in Chrome",
"configurations": [
"Launch Website in Chrome",
"Launch Server"
]
}
]
},

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2022 Manuel Thalmann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -137,7 +137,7 @@ export let Watch: TaskFunction = async (): Promise<void> =>
syncer.init({
open: false,
proxy: "http://localhost:1337",
server: join(context.StaticPath()),
online: false
});

View file

@ -1,26 +1,31 @@
{
"name": "connect-force",
"private": true,
"files": [],
"workspaces": {
"packages": [
"./packages/*"
]
},
"version": "0.0.0",
"type": "module",
"description": "A selfmade Connect Four game.",
"author": "Manuel Thalmann <m@nuth.ch>",
"scripts": {
"rebuild": "npm run --workspaces rebuild",
"watch": "concurrently --raw \"npm run --workspaces --if-present watch\"",
"clean": "npm run --workspaces clean",
"lint-local": "eslint --max-warnings 0 .eslintrc.cjs",
"lint-local-ide": "npm run lint-local || exit 0",
"lint": "npm run lint-local && npm run --workspaces lint",
"lint-ide": "npm run lint-local-ide && npm run --workspaces lint-ide",
"test": "npm run --workspaces test",
"gulp": "cross-env NODE_OPTIONS=\"--loader ts-node/esm\" gulp --",
"build": "npm run gulp Build",
"rebuild": "npm run clean && npm run build",
"watch": "npm run gulp Watch",
"clean": "rimraf ./lib",
"lint": "eslint --max-warnings 0 ./src .eslintrc.cjs",
"lint-ide": "npm run lint || exit 0",
"prepare": "npm run rebuild"
},
"devDependencies": {
"@manuth/eslint-plugin-typescript": "^4.0.1",
"concurrently": "^7.6.0",
"eslint": "^8.29.0"
"@manuth/tsconfig": "^3.0.2",
"@types/browser-sync": "^2.26.3",
"@types/gulp": "^4.0.10",
"@types/node": "^18.11.11",
"browser-sync": "^2.27.10",
"cross-env": "^7.0.3",
"eslint": "^8.29.0",
"gulp": "^4.0.2",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1",
"upath": "^2.0.1"
}
}

View file

@ -1,14 +0,0 @@
const { join } = require("node:path");
module.exports = {
env: {
browser: true
},
parserOptions: {
project: [
join(__dirname, "app.jsconfig.json"),
join(__dirname, "eslint.jsconfig.json"),
join(__dirname, "gulp.tsconfig.json")
]
}
};

View file

@ -1,168 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Source-files
[Ss]rc/
# TypeScript config-files
tsconfig.json
tsconfig.*.json
# Lint config-files
.eslintrc
.eslintrc.*
# Source-maps
[Ll]ib/**/*.map
# Unit-Tests
.mocharc.*
[Ll]ib/tests/
# Visual Studio Code-Environment
.vscode/
# GitHub configuration
.github/
# CI configuration
.drone.yml
.woodpecker.yml
# Build Environment
gulp/
gulpfile.ts
# Temporary release-assets
.tagName.txt
.tagHeading.txt
.releaseNotes.md
.releaseTitle.md

View file

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true
},
"include": [
"./.eslintrc.cjs"
]
}

View file

@ -1,31 +0,0 @@
{
"name": "connect-force",
"version": "0.0.0",
"type": "module",
"description": "A selfmade Connect Four game.",
"author": "Manuel Thalmann <m@nuth.ch>",
"scripts": {
"gulp": "cross-env NODE_OPTIONS=\"--loader ts-node/esm\" gulp --",
"build": "npm run gulp Build",
"rebuild": "npm run clean && npm run build",
"watch": "npm run gulp Watch",
"clean": "rimraf ./lib",
"lint": "eslint --max-warnings 0 ./src .eslintrc.cjs",
"lint-ide": "npm run lint || exit 0",
"prepare": "npm run rebuild"
},
"devDependencies": {
"@manuth/eslint-plugin-typescript": "^4.0.1",
"@manuth/tsconfig": "^3.0.2",
"@types/browser-sync": "^2.26.3",
"@types/gulp": "^4.0.10",
"@types/node": "^18.11.11",
"browser-sync": "^2.27.10",
"cross-env": "^7.0.3",
"eslint": "^8.29.0",
"gulp": "^4.0.2",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1",
"upath": "^2.0.1"
}
}

View file

@ -1,150 +0,0 @@
import { Game } from "./Game.js";
/**
* The game that is being played.
*
* @type {Game}
*/
let game;
/**
* A value indicating whether a transfer is pending.
*/
let transferPending = false;
/**
* The id of the save game.
*/
let id = "";
/**
* Gets the save button.
*
* @returns {HTMLButtonElement}
* The save button.
*/
function getSaveButton()
{
return document.querySelector(".save");
}
/**
* Gets the load button.
*
* @returns {HTMLButtonElement}
* The load button.
*/
function getLoadButton()
{
return document.querySelector(".load");
}
/**
* Gets an url for storing and loading the save game.
*
* @returns {URL}
* The url for storing and loading the save game.
*/
function getUrl()
{
let result = new URL(
id,
new URL(
"/api/data/",
window.location.origin));
result.searchParams.append("token", "c4game");
return result;
}
/**
* Initializes the board.
*/
function initialize()
{
game = new Game("game");
game.initialize();
(/** @type {HTMLElement} */ (document.querySelector(".new-game"))).onclick = (event) =>
{
event.preventDefault();
game.reset();
};
getSaveButton().onclick = async () =>
{
if (!transferPending)
{
transferPending = true;
getSaveButton().disabled = true;
getLoadButton().disabled = true;
try
{
if (id === "")
{
let result = await (await fetch(
getUrl(),
{
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(game.dump())
})).json();
({ id } = result);
}
else
{
await fetch(
getUrl(),
{
method: "PUT",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(game.dump())
});
}
}
catch { }
getSaveButton().disabled = false;
getLoadButton().disabled = false;
transferPending = false;
}
else
{
console.log("Already busy");
}
};
getLoadButton().onclick = async () =>
{
if (!transferPending)
{
transferPending = true;
getSaveButton().disabled = true;
getLoadButton().disabled = true;
try
{
game.load(
await (
await fetch(getUrl())).json());
}
catch { }
getSaveButton().disabled = false;
getLoadButton().disabled = false;
transferPending = false;
}
else
{
console.log("Already busy");
}
};
}
initialize();

View file

@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.base.json"
}

View file

@ -1,15 +0,0 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./app.jsconfig.json"
},
{
"path": "./eslint.jsconfig.json"
},
{
"path": "./gulp.tsconfig.json"
}
],
"include": []
}

View file

@ -1,13 +0,0 @@
const { join } = require("node:path");
module.exports = {
env: {
browser: true
},
parserOptions: {
project: [
join(__dirname, "app.jsconfig.json"),
join(__dirname, "eslint.jsconfig.json")
]
}
};

View file

@ -1,168 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Source-files
[Ss]rc/
# TypeScript config-files
tsconfig.json
tsconfig.*.json
# Lint config-files
.eslintrc
.eslintrc.*
# Source-maps
[Ll]ib/**/*.map
# Unit-Tests
.mocharc.*
[Ll]ib/tests/
# Visual Studio Code-Environment
.vscode/
# GitHub configuration
.github/
# CI configuration
.drone.yml
.woodpecker.yml
# Build Environment
gulp/
gulpfile.ts
# Temporary release-assets
.tagName.txt
.tagHeading.txt
.releaseNotes.md
.releaseTitle.md

View file

@ -1,11 +0,0 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"composite": true
},
"include": [
"./src/**/*"
]
}

View file

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true
},
"include": [
"./.eslintrc.cjs"
]
}

View file

@ -1,19 +0,0 @@
{
"name": "connect-force-server",
"version": "0.0.0",
"type": "module",
"description": "A server for Storing ConnectForce Savegames",
"author": "Manuel Thalmann <m@nuth.ch>",
"scripts": {
"start": "noderel -e ./src/main.js"
},
"dependencies": {
"express": "^4.18.2",
"randexp": "^0.5.3"
},
"devDependencies": {
"@types/express": "^4.17.15",
"@types/node": "^18.11.15",
"noderel": "^1.0.13"
}
}

View file

@ -1,27 +0,0 @@
/**
* Represents an http error.
*/
export class HTTPError extends Error
{
/**
* The http status code.
*
* @type {number}
*/
status;
/**
* Initializes a new instance of the {@link HTTPError `HTTPError`} class.
*
* @param {number} status
* The http status code.
*
* @param {string} message
* The error message.
*/
constructor(status, message)
{
super(message);
this.status = status;
}
}

View file

@ -1,150 +0,0 @@
import { join } from "path";
import { fileURLToPath } from "url";
import express from "express";
import RandExp from "randexp";
import { HTTPError } from "./HTTPError.js";
const { randexp } = RandExp;
const dirname = fileURLToPath(new URL(".", import.meta.url));
const app = express();
const apiKeys = [
"c4game"
];
const dataPath = "/api/data";
const parametrizedDataPath = "/api/data/:id";
/**
* The data provided by the api.
*
* @type {Record<string, unknown>}
*/
let data = {};
/**
* Creates a new guid.
*
* @returns {string}
* The newly created guid.
*/
function createGuid()
{
return randexp(/[0-9a-f]{8}(-[0-9a-f]{4}){4}[0-9a-f]{8}/);
}
app.use(express.static(join(dirname, "..", "..", "game", "lib", "static")));
app.use("/api", express.json());
app.use(
"/api",
(request, response, next) =>
{
const keyParam = "token";
if (keyParam in request.query)
{
let key = request.query[keyParam];
if (typeof key === "string" && apiKeys.includes(key))
{
next();
}
else
{
next(new HTTPError(401, "The specified API token is invalid"));
}
}
else
{
next(new HTTPError(401, "An API token is required"));
}
});
app.get(
parametrizedDataPath,
(request, response, next) =>
{
let id = request.params.id;
console.log(`Data ID \`${id}\` requested`);
if (id in data)
{
response.send(data[id]);
}
else
{
next();
}
});
app.post(
dataPath,
(request, response, next) =>
{
let id = createGuid();
data[id] = request.body;
response.send({ id });
});
app.put(
parametrizedDataPath,
(request, response, next) =>
{
let id = request.params.id;
if (id in data)
{
data[id] = request.body;
response.send(data[id]);
}
else
{
next();
}
});
app.delete(
parametrizedDataPath,
(request, response, next) =>
{
let id = request.params.id;
if (id in data)
{
delete data[id];
response.send();
response.status(204);
}
else
{
next();
}
});
app.use(
[
(error, request, response, next) =>
{
response.send(`${error}`);
response.status(error instanceof HTTPError ? error.status : 500);
}
]);
app.use(
"/api",
(request, response) =>
{
response.send({ error: "Not Found" });
response.status(404);
});
app.use(
(request, response) =>
{
response.send("Not Found");
response.status(404);
});
app.listen(1337);

View file

@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.base.json"
}

View file

@ -1,12 +0,0 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./app.jsconfig.json"
},
{
"path": "./eslint.jsconfig.json"
}
],
"include": []
}

61
src/js/main.js Normal file
View file

@ -0,0 +1,61 @@
import { Game } from "./Game.js";
/**
* The game that is being played.
*
* @type {Game}
*/
let game;
const saveGameKey = "connect-force-save";
/**
* Gets the save button.
*
* @returns {HTMLButtonElement}
* The save button.
*/
function getSaveButton()
{
return document.querySelector(".save");
}
/**
* Gets the load button.
*
* @returns {HTMLButtonElement}
* The load button.
*/
function getLoadButton()
{
return document.querySelector(".load");
}
/**
* Initializes the board.
*/
function initialize()
{
game = new Game("game");
game.initialize();
(/** @type {HTMLElement} */ (document.querySelector(".new-game"))).onclick = (event) =>
{
event.preventDefault();
game.reset();
};
getSaveButton().onclick = async (event) =>
{
event.preventDefault();
localStorage.setItem(saveGameKey, JSON.stringify(game.dump()));
};
getLoadButton().onclick = async (event) =>
{
event.preventDefault();
game.load(JSON.parse(localStorage.getItem(saveGameKey)));
};
}
initialize();

View file

@ -1,11 +1,14 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./app.jsconfig.json"
},
{
"path": "./eslint.jsconfig.json"
},
{
"path": "./packages/game"
"path": "./gulp.tsconfig.json"
}
],
"include": []