Skip to content

Commit

Permalink
Removed rule from everywhere. Added export/import feature. 1.0.0 rele…
Browse files Browse the repository at this point in the history
…ase time.
  • Loading branch information
Perlkonig committed Jul 15, 2022
1 parent f19b5a2 commit 8a6938b
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 72 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2022-07-15

### Added

* Added export/import feature. This includes all the guesses and koans but not the student list or guessing stone counts. You can export at any time. When the game ends, the code will also be provided.

### Removed

* Removed the secret rule from the game object and from the master's screen. There's no way to truly secure it in a peer-to-peer environment, and the intent of the app is to facilitate synchronous games, where the master shouldn't need a constant reminder. KISS, as they say.

## [0.4.0] - 2022-07-14

### Added
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Demo is currently at <https://www.perlkonig.com/zendo>.

When first opening the page, it creates a new game lobby and provides a client ID. One person needs to share that ID with their friends.

The other players enter that ID into the "Join" box and clicks "Join Game" to be connected with the others.
The other players enter that ID into the "Join" box and click "Join Game" to be connected with the others.

Once connected, players have access to a basic text chat window and the rest of the game interface. The game client does *not* offer things like video and voice chat features. Use services like Discord and Zoom instead. This client is designed just to help you moderate the game remotely. Should anybody become disconnected, they can simply "Join" using the ID of any player still in the game room.
Once connected, players have access to a shared game interface. The game client does *not* offer things like video and voice chat features. Use services like Discord and Zoom instead. This client is designed just to help you moderate the game remotely. Should anybody become disconnected, they can simply "Join" using the ID of any player still in the game room.

## Dojo Types

Expand All @@ -22,7 +22,3 @@ The following dojo types are supported:
* 1d pyramids (one-dimensional koans with [Looney pyramids](https://www.looneylabs.com/pyramids-home))
* dot matrix (coloured dots on rectangular grids)
* [GraphViz](https://graphviz.org/) (rendered by [dot](https://graphviz.org/docs/layouts/dot/))

## Caveats

There is no server! This means that clients can be hacked and people can cheat. Don't play with cheaters. Efforts have been made to prevent people from *accidentally* seeing the master's rule, but seeing it is indeed *possible* if the master enters it.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@abstractplay/zendo",
"private": true,
"version": "0.4.0",
"version": "1.0.0",
"description": "A peer-to-peer, synchronous client for playing Zendo online",
"author": "Aaron Dalton <[email protected]> (https://www.perlkonig.com)",
"license": "MIT",
Expand All @@ -10,6 +10,7 @@
"zendo",
"induction",
"logic",
"game",
"peer-to-peer"
],
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Game/ActionBar/AddKoan.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
</script>

<p class="control">
<button class="button" on:click="{() => {modalAdd = "is-active"}}">
<button class="button is-primary" on:click="{() => {modalAdd = "is-active"}}">
Add Koan
</button>
</p>
Expand Down
39 changes: 32 additions & 7 deletions src/lib/Game/ActionBar/GameOver.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { peers } from "@/stores/writePeers";
import { peer } from "@/stores/writePeerObj";
import { myName } from "@/stores/writeMyName";
import type { ZendoGameState } from "@/schemas/game";
const id2name = (id: string): string => {
const idx = $peers.findIndex((rec) => rec.id === id);
Expand All @@ -18,14 +19,38 @@
return id;
}
};
let exportedGame = {} as ZendoGameState;
if ($game.hasOwnProperty("koanType")) {
exportedGame.koanType = $game.koanType;
}
if ($game.hasOwnProperty("welcome")) {
exportedGame.welcome = $game.welcome;
}
if ($game.hasOwnProperty("guesses")) {
exportedGame.guesses = [...$game.guesses];
exportedGame.guesses.map(g => g.student = id2name(g.student));
}
if ($game.hasOwnProperty("koans")) {
exportedGame.koans = [...$game.koans];
}
let exportDataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportedGame));
</script>

<div class="box">
<p>The game is over</p>
{#if $game.winner !== null}
<p>The winner was {id2name($game.winner)} ({$game.winner}).</p>
{:else}
<p>There was no winner.</p>
{/if}
<p>To start a new game, refresh this window and join/host a new game.</p>
<div class="content">
<p>The game is over.</p>
{#if $game.winner !== null}
<p>The winner was {id2name($game.winner)} ({$game.winner}).</p>
{:else}
<p>There was no winner.</p>
{/if}
<p>To start a new game, refresh this window and join/host a new game.</p>
</div>
<hr>
<div class="content">
<p>The following code is a record of the game that can be reloaded into the client at a later time for review. Copy and paste it, or <a href="{exportDataStr}" download="ZendoGame_{(new Date()).toISOString()}.json">click here to download it</a>.</p>
<p><code>{JSON.stringify(exportedGame)}</code></p>
</div>
</div>
81 changes: 71 additions & 10 deletions src/lib/Game/ActionBar/Global.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,26 @@
import { peers } from "@/stores/writePeers";
import { game } from "@/stores/writeGame";
import { peer } from "@/stores/writePeerObj";
import { myName } from "@/stores/writeMyName";
import type { ZendoGameState } from "@/schemas/game";
import MarkKoan from "./Global/MarkKoan.svelte";
import PendingGuess from "./Global/PendingGuess.svelte";
const id2name = (id: string): string => {
const idx = $peers.findIndex((rec) => rec.id === id);
if (idx !== -1) {
if ($peers[idx].alias !== undefined) {
return $peers[idx].alias;
} else {
return id;
}
} else if (id === $peer.id) {
return $myName;
} else {
return id;
}
};
let modalAbandon = "";
const abandonGame = () => {
modalAbandon = "";
Expand Down Expand Up @@ -34,6 +51,27 @@
}
}
});
let modalExport = "";
let exportedGame: ZendoGameState;
let exportDataStr: string;
game.subscribe((obj) => {
exportedGame = {} as ZendoGameState;
if (obj.hasOwnProperty("koanType")) {
exportedGame.koanType = obj.koanType;
}
if (obj.hasOwnProperty("welcome")) {
exportedGame.welcome = obj.welcome;
}
if (obj.hasOwnProperty("guesses")) {
exportedGame.guesses = [...obj.guesses];
exportedGame.guesses.map(g => g.student = id2name(g.student));
}
if (obj.hasOwnProperty("koans")) {
exportedGame.koans = [...obj.koans];
}
exportDataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportedGame));
});
</script>

<div class="box">
Expand Down Expand Up @@ -64,12 +102,21 @@
<PendingGuess/>
{/if}

<div class="content space-above">
<p class="control">
<button class="button" on:click="{() => {modalAbandon = "is-active"}}">
Abandon Game
</button>
</p>
<div class="level">
<div class="level-item">
<p class="control">
<button class="button" on:click="{() => {modalExport = "is-active"}}">
Export Game
</button>
</p>
</div>
<div class="level-item">
<p class="control">
<button class="button" on:click="{() => {modalAbandon = "is-active"}}">
Abandon Game
</button>
</p>
</div>
</div>
</div>

Expand All @@ -80,7 +127,7 @@
<p class="modal-card-title">Are you sure you want to abandon the game?</p>
</header>
<section class="modal-card-body">
<p>Abandoning the game will cause you to disconnect from all peers. Your version of the game will be marked as terminated so you can see the final game state, including the secret rule if the master entered it. The game will continue for everybody else.</p>
<p>Abandoning the game will cause you to disconnect from all peers. Your version of the game will be marked as terminated, but the game will continue for everybody else.</p>
</section>
<footer class="modal-card-foot">
<button class="button is-success" on:click="{abandonGame}">Abandon Game</button>
Expand All @@ -89,8 +136,22 @@
</div>
</div>

<div class="modal {modalExport}" id="viewExport">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Export Game</p>
</header>
<section class="modal-card-body">
<p class="content">The following code contains all the current koans and guesses of this game. It does not include student names or guessing stone counts. But it's still a useful tool for studying a game or pausing it until later.</p>
<p class="content">You can copy and paste the code, or <a href="{exportDataStr}" download="ZendoGame_{(new Date()).toISOString()}.json">click here to download it</a>.</p>
<p class="content"><code>{JSON.stringify(exportedGame)}</code></p>
</section>
<footer class="modal-card-foot">
<button class="button is-success" on:click="{() => modalExport = ""}">Close</button>
</footer>
</div>
</div>

<style>
.space-above {
margin-top: 1em;
}
</style>
24 changes: 0 additions & 24 deletions src/lib/Game/ActionBar/Master.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,6 @@
}
}
const obfuscate = (txt: string = ""): string => {
return Buffer.from(txt, "binary").toString('base64');
};
const clarify = (txt: string = ""): string => {
return Buffer.from(txt, 'base64').toString('binary');
};
let ruleCommitted = true;
let clearRule = "";
game.subscribe((obj) => {clearRule = clarify(obj.rule)});
const handleRuleKeydown = (event) => {
if (event.key === 'Enter') {
$game.rule = obfuscate(clearRule);
pushGame();
ruleCommitted = true;
}
};
let selectedObserver = "";
const admit = () => {
if (selectedObserver.length > 0) {
Expand Down Expand Up @@ -102,13 +85,6 @@
<input class="input {welcomeCommitted ? "is-success" : "is-warning"}" type="text" placeholder="Pinned to the top of the play area. Supports Markdown." id="WelcomeMsg" bind:value="{$game.welcome}" on:keydown="{handleWelcomeKeydown}" on:input="{() => welcomeCommitted = false}">
</div>
</div>
<div class="field">
<label class="label" for="SecretRule">Secret Rule</label>
<div class="control">
<input class="input {ruleCommitted ? "is-success" : "is-warning"}" type="text" id="SecretRule" bind:value="{clearRule}" on:keydown="{handleRuleKeydown}" on:input="{() => ruleCommitted = false}">
</div>
<p class="help">Optional and generally not needed if you're already on a voice connection with everyone. It does make it easy for someone to take over for you, but it also means determined cheaters could see the rule.</p>
</div>
{#if $observers.length > 0}
<div class="field">
<label class="label" for="observerSelect">Select observers to admit as students</label>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Game/ActionBar/Master/ReviewGuess.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
<div class="card-footer">
<button class="button card-footer-item is-success" on:click="{acceptGuess}">Accept</button>
<button class="button card-footer-item is-danger" on:click="{rejectGuess}">Reject</button>
<button class="button card-footer-item is-warning" on:click="{() => modalWin = "is-active"}">WINNING GUESS!!</button>
<button class="button card-footer-item is-warning" on:click="{() => modalWin = "is-active"}">Enlightenment Achieved!!</button>
</div>
</div>
{:else}
Expand Down
68 changes: 62 additions & 6 deletions src/lib/Game/ActionBar/NewGame.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { peer } from "@/stores/writePeerObj";
import { peers } from "@/stores/writePeers";
import type { ZendoGameMessages } from "@/schemas/messages";
import type { ZendoGameState } from "@/schemas/game";
const pushGame = () => {
const msg: ZendoGameMessages = {
Expand All @@ -19,16 +20,71 @@
pushGame();
};
let modalImport = "";
let importedCode: string;
const importGame = () => {
let obj: ZendoGameState;
try {
obj = JSON.parse(importedCode) as ZendoGameState;
} catch {
return;
}
if ( (! obj.hasOwnProperty("koanType")) || (obj.koanType === undefined) ) {
return;
}
$game.master = $peer.id;
$game.koanType = obj.koanType;
if (obj.hasOwnProperty("welcome")) {
$game.welcome = obj.welcome;
}
if (obj.hasOwnProperty("guesses")) {
$game.guesses = [...obj.guesses];
}
if (obj.hasOwnProperty("koans")) {
$game.koans = [...obj.koans];
}
pushGame();
modalImport = "";
};
</script>

<div class="box">
<p class="content">
<a href="https://www.looneylabs.com/content/zendo">Game Rules</a>
</p>
<p class="control">
<button class="button" on:click="{becomeMaster}">
Become the Master
</button>
</p>
<div class="level">
<div class="level-item">
<p class="control">
<button class="button" on:click="{becomeMaster}">
Become the Master
</button>
</p>
</div>
<div class="level-item">
<p class="control">
<button class="button" on:click="{() => modalImport = "is-active"}">
Import Saved Game
</button>
</p>
</div>
</div>
</div>

<div class="modal {modalImport}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Import Game</p>
</header>
<section class="modal-card-body">
<p class="content">Paste the code you saved or downloaded previously. You will automatically become the master of the renewed dojo.</p>
<div class="control">
<textarea class="input" type="text" rows="5" placeholder="Paste code here" id="pastedCode" bind:value="{importedCode}"></textarea>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-primary" on:click="{importGame}">Import</button>
<button class="button is-success" on:click="{() => modalImport = ""}">Close</button>
</footer>
</div>
</div>
Loading

0 comments on commit 8a6938b

Please sign in to comment.