mirror of
https://github.com/ReVax-2007/cipher_master.git
synced 2026-09-17 21:32:27 +00:00
Compare commits
1 Commits
feature/fi
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 84fd2da49c |
86
README.md
86
README.md
@@ -23,7 +23,7 @@ An AI-powered desktop application built with **Rust** and **Tauri v2** that gene
|
|||||||
* **Affine Cipher**: Mathematical linear function cipher ($E(x) = (ax + b) \pmod{26}$).
|
* **Affine Cipher**: Mathematical linear function cipher ($E(x) = (ax + b) \pmod{26}$).
|
||||||
* **A1Z26 Numeric**: Positional index substitution.
|
* **A1Z26 Numeric**: Positional index substitution.
|
||||||
* **Fill in the Blank**: Assigns every letter a unique number from `01` to `26`, reveals a few mappings as clues, and preserves repeated-letter patterns.
|
* **Fill in the Blank**: Assigns every letter a unique number from `01` to `26`, reveals a few mappings as clues, and preserves repeated-letter patterns.
|
||||||
* **Local AI Integration & Fallback Support**: Connects to a local [Ollama](https://ollama.com/) instance (`llama3.2:1b`) to generate dynamic quotes. If Ollama is not installed or running, the engine automatically uses internal text prompts.
|
* **Local AI Integration & Fallback Support**: Connects to a local [Ollama](https://ollama.com/) instance (`llama3.2:1b`) to generate dynamic quotes. If Ollama is not installed or running, the engine selects a quote from `src-tauri/data/quotes.json`.
|
||||||
* **Difficulty Scaling**:
|
* **Difficulty Scaling**:
|
||||||
* **Easy**: Preserves original word boundaries, spaces, and punctuation. Generates detailed letter frequency and vowel count hints.
|
* **Easy**: Preserves original word boundaries, spaces, and punctuation. Generates detailed letter frequency and vowel count hints.
|
||||||
* **Medium**: Converts text to uppercase and strips all punctuation.
|
* **Medium**: Converts text to uppercase and strips all punctuation.
|
||||||
@@ -44,28 +44,73 @@ This mode creates a numeric substitution puzzle. Each letter is assigned a uniqu
|
|||||||
|
|
||||||
If you do not want to run Ollama locally or prefer using your own custom paragraphs, quotes, or text passages:
|
If you do not want to run Ollama locally or prefer using your own custom paragraphs, quotes, or text passages:
|
||||||
|
|
||||||
1. Open `src-tauri/src/lib.rs` in your text editor.
|
1. Edit `src-tauri/data/quotes.json` and add plain JSON strings to the array.
|
||||||
2. Locate the `fallback_quote` function:
|
2. Keep the file as valid JSON. Quotes are selected by word count when possible, then a random entry is used as a final fallback.
|
||||||
```rust
|
3. Rebuild or run `cargo tauri dev` to apply the updated text pool.
|
||||||
fn fallback_quote(length_choice: usize) -> String {
|
|
||||||
let topics = ["VIKINGS", "KNOWLEDGE", "FORTUNE", "STRATEGY", "FREEDOM"];
|
## Android Packaging
|
||||||
let actions = [
|
|
||||||
"SAILED ACROSS THE UNKNOWN",
|
Tauri v2 supports Android from the same project. Install Android Studio, the Android SDK, and the Rust Android targets, then run:
|
||||||
"DISCOVERED HIDDEN TRUTHS",
|
|
||||||
"FAVORED THE BRAVE MINDS",
|
```bash
|
||||||
"PROTECTED LIBERTY WITH SECRETS"
|
cargo tauri android init
|
||||||
];
|
cargo tauri android dev
|
||||||
// ...
|
cargo tauri android build
|
||||||
}
|
|
||||||
3. To hardcode a custom text passage or quote directly, modify the function to return your desired text string:
|
|
||||||
```rust
|
|
||||||
fn fallback_quote(_length_choice: usize) -> String {
|
|
||||||
"YOUR CUSTOM PARAGRAPH OR PHRASE GOES HERE".to_string()
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The generated Android project lives under `src-tauri/gen/android` and should be reviewed before committing. The responsive frontend and bundled quote library work without a desktop-only dependency; Ollama remains optional and is normally unavailable on a phone.
|
||||||
|
|
||||||
4. Rebuild or run `cargo tauri dev` to apply your custom text pool.
|
## Discord Slash Commands Setup
|
||||||
|
|
||||||
|
The opt-in starter bot lives in `integrations/discord-bot`. It supports `/cipher`, `/hint`, and `/solution`. Challenges are stored in memory per guild and user; hints and solutions are sent as ephemeral replies.
|
||||||
|
|
||||||
|
### 1. Create the Discord application
|
||||||
|
|
||||||
|
1. Open the [Discord Developer Portal](https://discord.com/developers/applications) and select **New Application**.
|
||||||
|
2. Open **General Information** and copy the **Application ID**. This is `DISCORD_CLIENT_ID`.
|
||||||
|
3. Open **Bot**, select **Reset Token**, and copy the token once. This is `DISCORD_TOKEN`. Treat it like a password and never commit it.
|
||||||
|
4. Under **OAuth2 > URL Generator**, select the `bot` and `applications.commands` scopes.
|
||||||
|
5. Select the `Send Messages` bot permission, open the generated URL, and invite the bot to a test server.
|
||||||
|
|
||||||
|
### 2. Install and configure the bot
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd integrations/discord-bot
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the credentials in the same terminal session. macOS/Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DISCORD_TOKEN="paste-your-bot-token-here"
|
||||||
|
export DISCORD_CLIENT_ID="paste-your-application-id-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:DISCORD_TOKEN = "paste-your-bot-token-here"
|
||||||
|
$env:DISCORD_CLIENT_ID = "paste-your-application-id-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register and run the commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run register
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The registration command installs the commands globally for the application. Discord can take up to an hour to propagate global commands. Keep `npm start` running while testing, or run it with a process manager for a hosted deployment.
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
|
||||||
|
* `/cipher` creates a Caesar or Atbash challenge. Use the optional `type` argument to choose the cipher.
|
||||||
|
* `/hint` returns a private hint for your current challenge.
|
||||||
|
* `/solution` returns the plaintext and key privately.
|
||||||
|
|
||||||
|
The bot uses the bundled `src-tauri/data/quotes.json` library. It does not call Ollama or persist challenges after restart. Add a database before running multiple bot instances or requiring durable challenge history.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -110,6 +155,7 @@ cipher_game/
|
|||||||
* [Rust](https://www.rust-lang.org/tools/install) (1.75 or newer)
|
* [Rust](https://www.rust-lang.org/tools/install) (1.75 or newer)
|
||||||
* `cargo-tauri` CLI (Install via `cargo install tauri-cli --version "^2.0"`)
|
* `cargo-tauri` CLI (Install via `cargo install tauri-cli --version "^2.0"`)
|
||||||
* *(Optional)* [Ollama](https://ollama.com/) running locally on port `11434` with `llama3.2:1b` installed (`ollama pull llama3.2:1b`).
|
* *(Optional)* [Ollama](https://ollama.com/) running locally on port `11434` with `llama3.2:1b` installed (`ollama pull llama3.2:1b`).
|
||||||
|
* *(Optional, Discord bot only)* [Node.js](https://nodejs.org/) 20 or newer and npm.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,30 @@ This document outlines proposed integrations with external services, tools, and
|
|||||||
|
|
||||||
## 4. Hardware & Peripherals
|
## 4. Hardware & Peripherals
|
||||||
* **Custom Printed Output (Thermal Printers)**: Direct USB/Bluetooth printer integration to print cryptographic escape room physical receipts/slips.
|
* **Custom Printed Output (Thermal Printers)**: Direct USB/Bluetooth printer integration to print cryptographic escape room physical receipts/slips.
|
||||||
|
|
||||||
|
## 5. Other OSes
|
||||||
|
* **Android Packaging**: Tauri Android support is ready to initialize with `cargo tauri android init` and build with `cargo tauri android build`. The responsive frontend and bundled quote fallback keep the mobile app useful without Ollama.
|
||||||
|
|
||||||
|
## 6. Discord Slash Commands
|
||||||
|
* **Discord bot starter**: `integrations/discord-bot` provides `/cipher`, `/hint`, and `/solution`. It uses the bundled quote library, keeps challenge state per user, and makes hints and solutions ephemeral.
|
||||||
|
* **Future bot engines**: Add the remaining Cipher Master algorithms behind the same command contract, then move challenge persistence to a small database if the bot is deployed across multiple instances.
|
||||||
|
|
||||||
|
### Discord Setup Quick Start
|
||||||
|
1. Create an application in the [Discord Developer Portal](https://discord.com/developers/applications).
|
||||||
|
2. Copy the Application ID from **General Information** and reset/copy the bot token from **Bot**.
|
||||||
|
3. In **OAuth2 > URL Generator**, select `bot` and `applications.commands`, plus the `Send Messages` permission, then invite the bot to a test server.
|
||||||
|
4. Run from the repository root:
|
||||||
|
```bash
|
||||||
|
cd integrations/discord-bot
|
||||||
|
npm install
|
||||||
|
export DISCORD_TOKEN="your-bot-token"
|
||||||
|
export DISCORD_CLIENT_ID="your-application-id"
|
||||||
|
npm run register
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
5. Test `/cipher`, then use `/hint` or `/solution`. Never commit `DISCORD_TOKEN`; global command registration can take up to an hour to appear.
|
||||||
|
|
||||||
|
## 7. Integration Principles
|
||||||
|
* Keep integrations opt-in and credentials in environment variables.
|
||||||
|
* Keep solutions private by default.
|
||||||
|
* Reuse the bundled quote fallback whenever an external AI provider is unavailable.
|
||||||
36
integrations/discord-bot/index.js
Normal file
36
integrations/discord-bot/index.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Client, GatewayIntentBits } from "discord.js";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
const quotes = JSON.parse(readFileSync(resolve(root, "src-tauri/data/quotes.json"), "utf8"));
|
||||||
|
const challenges = new Map();
|
||||||
|
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||||
|
|
||||||
|
function encrypt(text, type) {
|
||||||
|
if (type === "atbash") return text.replace(/[A-Z]/g, letter => String.fromCharCode(90 - (letter.charCodeAt(0) - 65)));
|
||||||
|
const shift = Math.floor(Math.random() * 25) + 1;
|
||||||
|
return { text: text.replace(/[A-Z]/g, letter => String.fromCharCode(65 + (letter.charCodeAt(0) - 65 + shift) % 26)), key: `Caesar shift ${shift}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
client.once("ready", () => console.log(`Cipher Master connected as ${client.user.tag}`));
|
||||||
|
client.on("interactionCreate", async interaction => {
|
||||||
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
const id = `${interaction.guildId}:${interaction.user.id}`;
|
||||||
|
if (interaction.commandName === "cipher") {
|
||||||
|
const type = interaction.options.getString("type") ?? "caesar";
|
||||||
|
const plaintext = quotes[Math.floor(Math.random() * quotes.length)].toUpperCase();
|
||||||
|
const encrypted = encrypt(plaintext, type);
|
||||||
|
const challenge = typeof encrypted === "string" ? { text: encrypted, key: "Atbash: reversed alphabet" } : encrypted;
|
||||||
|
challenges.set(id, { ...challenge, plaintext, type });
|
||||||
|
await interaction.reply({ content: `**${type.toUpperCase()} CHALLENGE**\n\n\`${challenge.text}\``, ephemeral: false });
|
||||||
|
} else if (interaction.commandName === "hint" || interaction.commandName === "solution") {
|
||||||
|
const challenge = challenges.get(id);
|
||||||
|
if (!challenge) return interaction.reply({ content: "Create a challenge first with `/cipher`.", ephemeral: true });
|
||||||
|
const content = interaction.commandName === "hint" ? `Cipher: **${challenge.type}**. ${challenge.type === "caesar" ? "Try counting the alphabet shift." : "Map each letter to its mirror in the alphabet."}` : `Plaintext: **${challenge.plaintext}**\nKey: **${challenge.key}**`;
|
||||||
|
await interaction.reply({ content, ephemeral: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(process.env.DISCORD_TOKEN);
|
||||||
7
integrations/discord-bot/package.json
Normal file
7
integrations/discord-bot/package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "cipher-master-discord-bot",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": { "start": "node index.js", "register": "node register-commands.js" },
|
||||||
|
"dependencies": { "discord.js": "^14.16.3" }
|
||||||
|
}
|
||||||
11
integrations/discord-bot/register-commands.js
Normal file
11
integrations/discord-bot/register-commands.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { REST, Routes, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
new SlashCommandBuilder().setName("cipher").setDescription("Create a Cipher Master challenge").addStringOption(option => option.setName("type").setDescription("Cipher to use").setRequired(false).addChoices({ name: "Caesar", value: "caesar" }, { name: "Atbash", value: "atbash" })),
|
||||||
|
new SlashCommandBuilder().setName("hint").setDescription("Get a private hint for your current challenge"),
|
||||||
|
new SlashCommandBuilder().setName("solution").setDescription("Get the private solution for your current challenge")
|
||||||
|
].map(command => command.toJSON());
|
||||||
|
|
||||||
|
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
|
||||||
|
await rest.put(Routes.applicationCommands(process.env.DISCORD_CLIENT_ID), { body: commands });
|
||||||
|
console.log("Registered /cipher, /hint, and /solution.");
|
||||||
@@ -2,120 +2,49 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Cipher Master</title>
|
<title>Cipher Master</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
:root { --ink: #18222d; --muted: #6a7783; --panel: #fffdf8; --line: #ded7c9; --copper: #b75d3b; --teal: #176b6b; }
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
* { box-sizing: border-box; }
|
||||||
background: #1e1e2e;
|
body { margin: 0; min-height: 100vh; color: var(--ink); font-family: Georgia, "Times New Roman", serif; background: radial-gradient(circle at 90% 5%, #f0c9a5 0, transparent 28%), linear-gradient(135deg, #f7f3ea, #dfe9e4); }
|
||||||
color: #cdd6f4;
|
main { width: min(1050px, 100%); margin: auto; padding: clamp(1.25rem, 4vw, 4rem); }
|
||||||
padding: 2rem;
|
header { display: flex; justify-content: space-between; gap: 1rem; align-items: end; margin-bottom: 2rem; }
|
||||||
margin: 0;
|
.eyebrow { color: var(--copper); font: 700 .75rem/1.2 system-ui, sans-serif; letter-spacing: .16em; text-transform: uppercase; }
|
||||||
}
|
h1 { margin: .35rem 0 0; font-size: clamp(2.1rem, 6vw, 4.6rem); line-height: .95; }
|
||||||
h1 { color: #89b4fa; margin-top: 0; }
|
.mark { color: var(--teal); font-size: 2.5rem; }
|
||||||
.form-group { margin-bottom: 1.2rem; }
|
.layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, .8fr); gap: 1.25rem; }
|
||||||
label { display: block; margin-bottom: 0.4rem; color: #a6adc8; }
|
.panel { background: color-mix(in srgb, var(--panel) 92%, transparent); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 18px 45px #293b3214; padding: clamp(1.1rem, 3vw, 2rem); }
|
||||||
select, button {
|
.panel h2 { margin: 0 0 1.35rem; font-size: 1.35rem; }
|
||||||
width: 100%;
|
.fields { display: grid; gap: 1rem; }
|
||||||
padding: 0.6rem;
|
label { display: grid; gap: .42rem; color: var(--muted); font: 700 .75rem system-ui, sans-serif; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
border-radius: 6px;
|
select, button { width: 100%; min-height: 48px; border: 1px solid var(--line); border-radius: 5px; padding: .75rem .85rem; font: 1rem system-ui, sans-serif; }
|
||||||
border: 1px solid #45475a;
|
select { color: var(--ink); background: #fff; }
|
||||||
background: #313244;
|
button { margin-top: 1.3rem; border: 0; color: #fff; background: var(--teal); font-weight: 700; cursor: pointer; transition: transform .2s, background .2s; }
|
||||||
color: #cdd6f4;
|
button:hover { background: #0e5252; transform: translateY(-2px); }
|
||||||
font-size: 1rem;
|
button:disabled { cursor: wait; opacity: .65; transform: none; }
|
||||||
}
|
.side { display: grid; align-content: start; gap: 1rem; }
|
||||||
button {
|
.status { color: var(--teal); font: 700 .78rem system-ui, sans-serif; text-transform: uppercase; letter-spacing: .08em; }
|
||||||
background: #89b4fa;
|
#output { min-height: 235px; margin: 0; padding: 1rem; overflow: auto; white-space: pre-wrap; color: #f9f4e9; background: #18222d; border-radius: 5px; font: .85rem/1.55 ui-monospace, SFMono-Regular, monospace; }
|
||||||
color: #11111b;
|
.note { color: var(--muted); font: .92rem/1.5 system-ui, sans-serif; }
|
||||||
font-weight: bold;
|
@media (max-width: 720px) { header { align-items: start; } .layout { grid-template-columns: 1fr; } .mark { font-size: 1.8rem; } }
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 1rem;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
button:hover { background: #b4befe; }
|
|
||||||
#output {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #181825;
|
|
||||||
border-radius: 6px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family: monospace;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>ᛏ Cipher Master Engine</h1>
|
<main>
|
||||||
|
<header><div><div class="eyebrow">A field kit for curious minds</div><h1><span class="mark">ᛏ</span> Cipher Master</h1></div><div class="status" id="status">Ready to encrypt</div></header>
|
||||||
<div class="form-group">
|
<section class="layout">
|
||||||
<label for="cipher">Cipher Type:</label>
|
<div class="panel"><h2>Compose a challenge</h2><div class="fields">
|
||||||
<select id="cipher">
|
<label for="cipher">Cipher<select id="cipher"><option value="CustomFuthark">Elder Futhark Runes</option><option value="Caesar">Caesar Shift</option><option value="Atbash">Atbash Cipher</option><option value="Substitution">Monoalphabetic Substitution</option><option value="Vigenere">Vigenere Cipher</option><option value="RailFence">Rail Fence</option><option value="Affine">Affine Cipher</option><option value="A1Z26">A1Z26 Numeric</option><option value="FillInTheBlank">Fill in the Blank</option></select></label>
|
||||||
<option value="CustomFuthark">Elder Futhark Runes</option>
|
<label for="difficulty">Difficulty<select id="difficulty"><option value="Easy">Easy - spaces and punctuation</option><option value="Medium">Medium - uppercase text</option><option value="Hard">Hard - five character blocks</option></select></label>
|
||||||
<option value="Caesar">Caesar Shift</option>
|
<label for="length">Quote length<select id="length"><option value="1">Short</option><option value="2">Medium</option><option value="3">Long</option></select></label>
|
||||||
<option value="Atbash">Atbash Cipher</option>
|
</div><button id="generate" onclick="generateCipher()">Generate challenge</button></div>
|
||||||
<option value="Substitution">Monoalphabetic Substitution</option>
|
<aside class="side"><div class="panel"><h2>Challenge output</h2><pre id="output">Your encrypted challenge and Desktop file paths will appear here.</pre></div><p class="note">Ollama is optional. When no model is available, Cipher Master selects a quote from its bundled local library.</p></aside>
|
||||||
<option value="Vigenere">Vigenère Cipher</option>
|
</section>
|
||||||
<option value="RailFence">Rail Fence (Zig-Zag)</option>
|
</main>
|
||||||
<option value="Affine">Affine Cipher</option>
|
|
||||||
<option value="A1Z26">A1Z26 Numeric</option>
|
|
||||||
<option value="FillInTheBlank">Fill in the Blank (Number Mapping)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="difficulty">Difficulty:</label>
|
|
||||||
<select id="difficulty">
|
|
||||||
<option value="Easy">Easy (Preserves spaces & punctuation)</option>
|
|
||||||
<option value="Medium">Medium (Uppercase, no punctuation)</option>
|
|
||||||
<option value="Hard">Hard (Grouped 5-character blocks)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="length">Text Length:</label>
|
|
||||||
<select id="length">
|
|
||||||
<option value="1">Short (~5-8 words)</option>
|
|
||||||
<option value="2">Medium (~15-20 words)</option>
|
|
||||||
<option value="3">Long (~30+ words)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="generateCipher()">Generate Challenge Desktop Files</button>
|
|
||||||
|
|
||||||
<div id="output">Outputs will appear here...</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function getInvoke() {
|
function getInvoke() { const tauri = window.__TAURI__; if (tauri?.core?.invoke) return tauri.core.invoke; if (tauri?.invoke) return tauri.invoke; if (window.__TAURI_INTERNALS__?.invoke) return window.__TAURI_INTERNALS__.invoke; throw new Error("Tauri IPC bindings not found."); }
|
||||||
if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
|
async function generateCipher() { const button = document.getElementById("generate"); const output = document.getElementById("output"); const status = document.getElementById("status"); button.disabled = true; status.textContent = "Forging challenge..."; output.textContent = "Selecting quote and encrypting..."; try { output.textContent = await getInvoke()("generate_cipher_challenge", { cipher_type: cipher.value, difficulty: difficulty.value, length_choice: Number(length.value) }); status.textContent = "Challenge ready"; } catch (error) { output.textContent = "Error: " + (error.message || error); status.textContent = "Generation failed"; } finally { button.disabled = false; } }
|
||||||
return window.__TAURI__.core.invoke;
|
|
||||||
} else if (window.__TAURI__ && window.__TAURI__.invoke) {
|
|
||||||
return window.__TAURI__.invoke;
|
|
||||||
} else if (window.__TAURI_INTERNALS__ && window.__TAURI_INTERNALS__.invoke) {
|
|
||||||
return window.__TAURI_INTERNALS__.invoke;
|
|
||||||
}
|
|
||||||
throw new Error("Tauri IPC bindings not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateCipher() {
|
|
||||||
const cipher = document.getElementById("cipher").value;
|
|
||||||
const difficulty = document.getElementById("difficulty").value;
|
|
||||||
const lengthChoice = parseInt(document.getElementById("length").value);
|
|
||||||
const output = document.getElementById("output");
|
|
||||||
|
|
||||||
output.innerText = "Querying local AI & generating cipher files...";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const invokeFn = getInvoke();
|
|
||||||
const response = await invokeFn("generate_cipher_challenge", {
|
|
||||||
cipher_type: cipher,
|
|
||||||
difficulty: difficulty,
|
|
||||||
length_choice: lengthChoice
|
|
||||||
});
|
|
||||||
output.innerText = response;
|
|
||||||
} catch (err) {
|
|
||||||
output.innerText = "Error: " + (err.message || err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
12
src-tauri/data/quotes.json
Normal file
12
src-tauri/data/quotes.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
"Courage begins where certainty ends.",
|
||||||
|
"The patient mind finds patterns in the noise.",
|
||||||
|
"A clear question can unlock a hidden door.",
|
||||||
|
"Wisdom grows when curiosity survives the first mistake.",
|
||||||
|
"The map is useful, but the journey teaches the terrain.",
|
||||||
|
"Small clues become powerful when connected with care.",
|
||||||
|
"The strongest key is often the question nobody thought to ask.",
|
||||||
|
"A secret loses its power when a brave mind learns how to read it.",
|
||||||
|
"Every complex puzzle becomes smaller when its rules are made visible.",
|
||||||
|
"Knowledge is not a locked room; it is a path made by patient footsteps."
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
use rand::seq::IteratorRandom;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
@@ -39,6 +40,11 @@ struct OllamaResponse {
|
|||||||
response: String,
|
response: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bundled_quotes() -> Vec<String> {
|
||||||
|
serde_json::from_str(include_str!("../data/quotes.json"))
|
||||||
|
.expect("bundled quote data must be valid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command(rename_all = "snake_case")]
|
#[tauri::command(rename_all = "snake_case")]
|
||||||
fn generate_cipher_challenge(
|
fn generate_cipher_challenge(
|
||||||
cipher_type: String,
|
cipher_type: String,
|
||||||
@@ -143,16 +149,21 @@ fn get_ai_quote(length_choice: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fallback_quote(length_choice: usize) -> String {
|
fn fallback_quote(length_choice: usize) -> String {
|
||||||
let topics = ["VIKINGS", "KNOWLEDGE", "FORTUNE", "STRATEGY", "FREEDOM"];
|
let quotes = bundled_quotes();
|
||||||
let actions = ["SAILED ACROSS THE UNKNOWN", "DISCOVERED HIDDEN TRUTHS", "FAVORED THE BRAVE MINDS", "PROTECTED LIBERTY WITH SECRETS"];
|
let target_range = match length_choice {
|
||||||
|
1 => 5..=10,
|
||||||
|
2 => 11..=20,
|
||||||
|
_ => 21..=40,
|
||||||
|
};
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
let base = format!("{} {}", topics.choose(&mut rng).unwrap(), actions.choose(&mut rng).unwrap());
|
quotes
|
||||||
if length_choice > 1 {
|
.iter()
|
||||||
format!("{} AND UNLOCKED THE SECRETS OF THE ANCIENT RUNES", base)
|
.filter(|quote| target_range.contains("e.split_whitespace().count()))
|
||||||
} else {
|
.choose(&mut rng)
|
||||||
base
|
.or_else(|| quotes.choose(&mut rng))
|
||||||
}
|
.expect("quote fallback data must not be empty")
|
||||||
|
.to_uppercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_desktop_path() -> PathBuf {
|
fn get_desktop_path() -> PathBuf {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Cipher Master",
|
"productName": "Cipher Master",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"identifier": "com.xvoet.ciphermaster",
|
"identifier": "com.xvoet.ciphermaster",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../public"
|
"frontendDist": "../public"
|
||||||
|
|||||||
Reference in New Issue
Block a user