Compare commits

..

No commits in common. "9107c557afa3fbf98d2be164b801b3653456a2b3" and "c40492c0e312ca0d0b9204897fbe0020eaff8cc0" have entirely different histories.

35 changed files with 862 additions and 99 deletions

View file

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

View file

@ -1,6 +1,15 @@
{
"folders": [
{
"name": "ConnectForce",
"path": "./packages/game"
},
{
"name": "Server",
"path": "./packages/server"
},
{
"name": "Solution Items",
"path": "."
}
],
@ -26,7 +35,7 @@
"label": "Build",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder:Solution Items}"
},
"command": "npm",
"args": [
@ -63,7 +72,7 @@
"label": "Rebuild",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder:Solution Items}"
},
"command": "npm",
"args": [
@ -79,7 +88,7 @@
"label": "Lint",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}"
"cwd": "${workspaceFolder:Solution Items}"
},
"command": "npm",
"args": [
@ -104,11 +113,33 @@
"request": "launch",
"name": "Launch Website in Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/lib/static",
"webRoot": "${workspaceFolder:ConnectForce}/lib/static",
"preLaunchTask": "Build",
"pathMapping": {
"/": "${workspaceFolder}/src"
"/": "${workspaceFolder:ConnectForce}/src"
},
"presentation": {
"hidden": true
}
},
{
"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) 2022 Manuel Thalmann
Copyright (c) <year> <copyright holders>
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

@ -1,31 +1,26 @@
{
"name": "connect-force",
"version": "0.0.0",
"type": "module",
"description": "A selfmade Connect Four game.",
"author": "Manuel Thalmann <m@nuth.ch>",
"private": true,
"files": [],
"workspaces": {
"packages": [
"./packages/*"
]
},
"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",
"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",
"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"
"concurrently": "^7.6.0",
"eslint": "^8.29.0"
}
}

View file

@ -0,0 +1,14 @@
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")
]
}
};

168
packages/game/.npmignore Normal file
View file

@ -0,0 +1,168 @@
# 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

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

View file

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

View file

@ -0,0 +1,31 @@
{
"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

@ -0,0 +1,150 @@
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

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

View file

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

View file

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

168
packages/server/.npmignore Normal file
View file

@ -0,0 +1,168 @@
# 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

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

View file

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

View file

@ -0,0 +1,19 @@
{
"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

@ -0,0 +1,27 @@
/**
* 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;
}
}

150
packages/server/src/main.js Normal file
View file

@ -0,0 +1,150 @@
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

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

View file

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

View file

@ -1,61 +0,0 @@
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,14 +1,11 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./app.jsconfig.json"
},
{
"path": "./eslint.jsconfig.json"
},
{
"path": "./gulp.tsconfig.json"
"path": "./packages/game"
}
],
"include": []