Compare commits

...

5 commits

Author SHA1 Message Date
Manuel Thalmann f8ae416fb4 Add a documentation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-12-23 20:24:25 +01:00
Manuel Thalmann 3a62c8902f Add a favicon 2022-12-23 18:28:42 +01:00
Manuel Thalmann b17363b9e8 Improve the look and feel of the log banner 2022-12-23 18:13:41 +01:00
Manuel Thalmann de0b81efac Add an ad banner 2022-12-23 18:13:25 +01:00
Manuel Thalmann c130b72ab6 Fix broken type declaration 2022-12-23 18:09:06 +01:00
13 changed files with 214 additions and 6 deletions

BIN
docs/DiagonalDown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/DiagonalUp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

143
docs/Documentation.md Normal file
View file

@ -0,0 +1,143 @@
---
Header:
Right: ConnectForce
---
# ConnectForce Dokumentation
> `ConnectForce` ist eine grossartige Umsetzung des allzeit beliebten Spiels namens "4 Gewinnt!".
> In diesem strategisch anspruchsvollen Spiel müssen Spieler ihre Spielsteine möglichst geschickt platzieren, um so 4 von ihnen zu verbinden und sich so als dem Gegner überlegen zu beweisen!
>
> Wirst du dazu imstande sein, über dem IQ und der Geschicklichkeit deines Gegners zu obsiegen?
Für die Umsetzung von `ConnectForce` wurden Standards und Technologien wie etwa `SJDON`, `SuiWeb`, JavaScript, CSS und HTML verwendet.
Das Ziel dieses Dokuments ist es, aufzuzeigen, welche Aspekte des Projekts in ihrer Umsetzung herausfordernd waren, wie diese Herausforderungen in Angriff genommen wurden und welche Fortschritte besonders erwähnenswert sind.
## Das Endprodukt
Das Endprodukt ist ein "4 Gewinnt!" Spiel, welches sich in der Grösse automatisch so skaliert, dass es sich auf jeglichen Geräten übersichtlich betrachten und auch steuern lässt (wie etwa Tablets, Handys, Computer, Ultra Widescreen Monitore etc.).
Folgender Screenshot zeigt, wie die Anwendung in einem Chromium Browser aussieht:
![](Preview.png)
## Features
Die Anwendung bringt einige nennenswerte Funktionen mit sich. Zu diesen zählen folgende:
- Die Fähigkeit, das Spiel jederzeit neu zu starten
- Die Möglichkeit, das Spiel im lokalen Speicher des Browsers zu speichern und aus diesem zu laden
- Die Funktion, einzelne Änderungen, die am Zustand des Spiels vorgenommen wurden, rückgängig zu machen
## Nutzung
Die Anwendung wird mit Hilfe der `SJDON`-Notation zur Verfügung gestellt und kann somit problemlos mit der Bibliothek `SuiWeb` dargestellt und ausgeführt werden:
```js
import { App } from "./Components.js";
import { render } from "./SuiWeb.js";
/**
* Initializes the board.
*/
function initialize()
{
render([App], document.querySelector("#game"));
}
initialize();
```
## Herausforderungen
Einige Aspekte des Spiels stellten sich, obwohl es zunächst nicht den Anschein machte, als Herausforderungen heraus. Eine davon wird in diesem Kapitel im Details beschrieben.
### Bestimmung des Gewinners
Teil der Aufgabe war es, anhand des Zustandes des Spielfelds zu entscheiden, ob und welcher Spieler gewonnen hat.
Der Weg für die Bestimmung des Gewinners, welcher in diesem Projekt verwendet wurde, wird im Folgenden genauer erklärt.
#### Grundlagen
Es gibt genau 4 verschiedene Arten, in denen Figuren zueinander stehen können:
| Bezeichnung | Relative Koordinaten |
| --------------------- | -------------------- |
| Horizontal | `[0, 1]` |
| Vertikal | `[1, 0]` |
| Diagonal (nach unten) | `[1, 1]` |
| Diagonal (nach oben) | `[1, -1]` |
Folgende Bilder dienen der Illustration:
- Horizontal:
![](./Horizontal.png)
- Vertikal:
![](./Vertical.png)
- Diagonal (nach unten):
![](./DiagonalDown.png)
- Diagonal (nach oben):
![](./DiagonalUp.png)
Der Fakt, dass Chips nur in diesen 4 Relationen auftreten können, wird genutzt, um mit Hilfe mehrerer Iterationen zu prüfen, ob sich jeweils 4 Felder mit den genannten relativen Koordinaten zueinander in Relation stehen.
Für die Kontrolle, ob eine Person gewonnen hat, wird folgender Algorithmus verwendet:
```js
// Iterate over all possible relative coordinates.
for (let yOffset = 0; yOffset <= 1; yOffset++)
{
for (let xOffset = (yOffset === 1) ? -1 : 1; xOffset <= 1; xOffset++)
{
// Calculate upper and lower bounds of the x-axis.
let lowerBound = Math.max(0, xOffset * (Game.#count - 1) * -1);
let upperBound = Math.min(Game.#width, Game.#width - (xOffset * (Game.#count - 1)));
// Iterate over all possible x- and y-coordinates.
for (let y = 0; y < (Game.#height - yOffset * (Game.#count - 1)); y++)
{
for (let x = lowerBound; x < upperBound; x++)
{
/**
* @type {CellOwner[]}
*/
let tokens = [];
// Generate a list of all tokens in the current scope.
for (let i = 0; i < Game.#count; i++)
{
tokens.push(this.board[y + i * yOffset][x + i * xOffset]);
}
let player = tokens[0];
// Check whether the tokens indicate a win.
if (
player !== "" &&
tokens.every((token) => token === player))
{
return player;
}
}
}
}
}
return null;
```
Dieser Algorithmus ist dazu imstande, zu bestimmen, ob und welcher Spieler gewonnen hat. Dies funktioniert unabhängig davon, ob die Steine horizontal, vertikal oder diagonal verbunden sind.
Einige Kommentare im Code sollen dabei behilflich sein, die Funktionalität zu verstehen.
## Umsetzung
Für das Umsetzen des Projekts wurde die Notation `SJDON` verwendet zusammen mit der ausgelieferten Bibliothek `SuiWeb` verwendet.
Komponenten mit Besonderheiten werden im folgenden Kapitel genauer beschrieben.
### Komponenten
#### App
Die App-Komponente beinhaltet das gesamte Spiel-Konstrukt, zeigt den aktuellen Zustand des Spiels an und verarbeitet Interaktionen, die der Benutzer mit dem Spiel macht.
Dies beinhaltet alle zuvor genannten Funktionen und das Vollziehen eines Spielzuges.
Zudem wird ein zum Aufrufenden ideal passendes Produkt in einem Werbungs-Banner angezeigt.
## MenuBar
Die Menü-Leiste erlaubt es dem Besucher, die von ihm gewünschten Funktionen auszuführen.
Sollte eine Funktion nicht verfügbar sein, wird dies dem Nutzer auf verständlicher Weise angezeigt.

BIN
docs/Horizontal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/Preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/Vertical.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -44,6 +44,18 @@ export function Assets(): NodeJS.ReadWriteStream
dest(context.StaticPath(context.AssetDirName)));
}
/**
* Builds the icon of the webpage.
*
* @returns
* The resulting stream.
*/
export function Icon(): NodeJS.ReadWriteStream
{
return src(context.SourcePath("favicon.ico")).pipe(
dest(context.StaticPath()));
}
/**
* Builds the web pages.
*
@ -69,6 +81,7 @@ export function Build(): Promise<void>
JavaScript,
Styles,
Assets,
Icon,
WebPages
])(
(error) =>
@ -159,6 +172,12 @@ export let Watch: TaskFunction = async (): Promise<void> =>
Assets,
BrowserSync(syncer, join(context.AssetDirName, "**", "*"))));
watch(
context.SourcePath("favicon.ico"),
series(
Icon,
BrowserSync(syncer, join("favicon.ico"))));
watch(
context.SourcePath("**", "*.html"),
series(

BIN
src/assets/ad.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
src/assets/adInfo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View file

@ -81,7 +81,7 @@ export function App()
}
},
{
className: `game ${game.winner ? `winner ${game.winner}` : ""}`
className: `game ${game.winner ? `winner ${game.winner}` : game.currentPlayer}`
},
[
Board,
@ -94,7 +94,8 @@ export function App()
`Player ${Constants.PLAYER_NAMES[game.winner]} wins!` :
`It's player "${Constants.PLAYER_NAMES[game.currentPlayer]}"s turn`
],
[MenuBar, { game }]
[MenuBar, { game }],
[Ad]
];
}
@ -198,3 +199,41 @@ export function Button({ content })
...content
];
}
/**
* Renders a very serious ad.
*
* @returns {NodeDescriptor}
* The rendered ad.
*/
export function Ad()
{
return [
"div",
{
className: "ad",
style: "text-align: center;"
},
[
"div",
{
style: "display: inline-block; position: relative;"
},
[
"a",
{
href: "https://www.youtube.com/watch?v=Wgt7JQdymsQ",
target: "_blank"
},
["img", { src: "./assets/ad.gif" }],
[
"img",
{
src: "./assets/adInfo.png",
style: "position: absolute; top: 0; right: 0;"
}
]
]
]
];
}

5
src/js/types.d.ts vendored
View file

@ -48,12 +48,13 @@ type TextDescriptor = string;
type ElementDescriptor = [
tag: string,
// eslint-disable-next-line @typescript-eslint/array-type
...args: (NodeDescriptor | (Partial<HTMLElement> & Record<string, any>))[]
...args: (NodeDescriptor | (Partial<HTMLElement> | Record<string, any>))[]
];
/**
* Represents a component in the SJDON notation.
*/
type FunctionNode = [
fn: (...args: any[]) => NodeDescriptor, ...args: any[]
fn: (...args: any[]) => NodeDescriptor,
...args: any[]
];

View file

@ -44,11 +44,11 @@ div {
float: left;
}
.game.winner.r .board, .game.winner.r .log {
.game.winner.r .board, .game.r .log {
background-color: pink;
}
.game.winner.b .board, .game.winner.b .log {
.game.winner.b .board, .game.b .log {
background-color: lightblue;
}
@ -73,6 +73,8 @@ div {
.log {
margin-top: 10px;
margin-bottom: 10px;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
text-align: center;
}
@ -87,3 +89,7 @@ div {
.game .menu-bar button {
margin: 0 5px 5px 0;
}
.ad {
margin-top: 3rem;
}