5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a2a8e0199a Fix Android workflow SDK setup package selection
Co-authored-by: ReVax-2007 <152216732+ReVax-2007@users.noreply.github.com>
2026-09-16 21:28:06 +00:00
copilot-swe-agent[bot]
ca00312695 Initial plan 2026-09-16 21:24:42 +00:00
Xavier Voet
239dd0ad35 Merge pull request #6 from ReVax-2007/feature/new-integrations
## Summary

Adds the new integration and mobile-readiness features for Cipher Master.

## Changes

- Added bundled JSON quote fallback at `src-tauri/data/quotes.json`
- Updated Rust quote generation to use the fallback library when Ollama is unavailable
- Refreshed the responsive frontend UI
- Added Discord slash-command starter integration
- Added `/cipher`, `/hint`, and `/solution`
- Made Discord hints and solutions ephemeral
- Added Discord Developer Portal setup documentation
- Added Android packaging instructions
- Updated Tauri application version to `1.1.0`
- Updated README and integration documentation

## Discord Setup

The Discord integration includes:

- `integrations/discord-bot/index.js`
- `integrations/discord-bot/register-commands.js`
- `integrations/discord-bot/package.json`

Supported commands:

- `/cipher`
- `/hint`
- `/solution`

## Release

- Release tag: `v1.1.0`
- Feature branch: `feature/new-integrations`

## Validation

- Discord JavaScript syntax validated
- Quote JSON validated
- Rust diagnostics checked
- Committed diff passes `git diff --check`

## Android Note

The documentation includes the Tauri Android initialization and build commands. The current release workflow still produces desktop artifacts only. Android project initialization and Android CI packaging should be added separately before publishing an Android binary.
2026-09-16 17:16:40 -04:00
84fd2da49c feat: add integrations and mobile-ready setup
Add bundled JSON quote fallback, responsive UI, Discord slash commands, Android setup documentation, and complete integration installation instructions. Set the Tauri application version to 1.1.0 for the release.
2026-09-16 17:03:27 -04:00
Xavier Voet
7cddbcedf2 Merge pull request #5 from ReVax-2007/feature/fill-in-the-blank
feat: add fill-in-the-blank cipher mode
2026-09-16 14:01:41 -04:00
10 changed files with 275 additions and 139 deletions

View File

@@ -48,4 +48,61 @@ jobs:
releaseName: 'Cipher Master ${{ github.ref_name }}' releaseName: 'Cipher Master ${{ github.ref_name }}'
releaseBody: 'Download the compiled standalone application binaries below.' releaseBody: 'Download the compiled standalone application binaries below.'
releaseDraft: true releaseDraft: true
prerelease: false prerelease: false
android:
name: Android APK and AAB
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
packages: ''
- name: Install Android SDK packages
run: sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" "ndk;27.2.12479018"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android
- name: Install Tauri CLI
run: cargo install tauri-cli --version '^2.0' --locked
- name: Initialize Android project
working-directory: src-tauri
run: cargo tauri android init
- name: Build Android packages
working-directory: src-tauri
run: cargo tauri android build --apk --aab
- name: Upload Android packages
uses: actions/upload-artifact@v4
with:
name: cipher-master-android-${{ github.ref_name }}
path: |
src-tauri/gen/android/app/build/outputs/apk/**/*.apk
src-tauri/gen/android/app/build/outputs/bundle/**/*.aab
if-no-files-found: error
- name: Attach Android packages to GitHub release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
src-tauri/gen/android/app/build/outputs/apk/**/*.apk
src-tauri/gen/android/app/build/outputs/bundle/**/*.aab
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -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.
--- ---

View File

@@ -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.

View 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);

View 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" }
}

View 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.");

View File

@@ -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>

View 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."
]

View File

@@ -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(&quote.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 {

View File

@@ -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"