diff --git a/README.md b/README.md index 822c0f5..c74c3a4 100644 --- a/README.md +++ b/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}$). * **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. -* **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**: * **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. @@ -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: -1. Open `src-tauri/src/lib.rs` in your text editor. -2. Locate the `fallback_quote` function: - ```rust - fn fallback_quote(length_choice: usize) -> String { - let topics = ["VIKINGS", "KNOWLEDGE", "FORTUNE", "STRATEGY", "FREEDOM"]; - let actions = [ - "SAILED ACROSS THE UNKNOWN", - "DISCOVERED HIDDEN TRUTHS", - "FAVORED THE BRAVE MINDS", - "PROTECTED LIBERTY WITH SECRETS" - ]; - // ... - } -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() -} +1. Edit `src-tauri/data/quotes.json` and add plain JSON strings to the array. +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. +3. Rebuild or run `cargo tauri dev` to apply the updated text pool. + +## Android Packaging + +Tauri v2 supports Android from the same project. Install Android Studio, the Android SDK, and the Rust Android targets, then run: + +```bash +cargo tauri android init +cargo tauri android dev +cargo tauri android build ``` +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) * `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, Discord bot only)* [Node.js](https://nodejs.org/) 20 or newer and npm. --- diff --git a/SUGGESTED_INTEGRATIONS.md b/SUGGESTED_INTEGRATIONS.md index 3cab8b1..4221236 100644 --- a/SUGGESTED_INTEGRATIONS.md +++ b/SUGGESTED_INTEGRATIONS.md @@ -18,3 +18,30 @@ This document outlines proposed integrations with external services, tools, and ## 4. Hardware & Peripherals * **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. \ No newline at end of file diff --git a/integrations/discord-bot/index.js b/integrations/discord-bot/index.js new file mode 100644 index 0000000..de7858d --- /dev/null +++ b/integrations/discord-bot/index.js @@ -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); diff --git a/integrations/discord-bot/package.json b/integrations/discord-bot/package.json new file mode 100644 index 0000000..d021321 --- /dev/null +++ b/integrations/discord-bot/package.json @@ -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" } +} diff --git a/integrations/discord-bot/register-commands.js b/integrations/discord-bot/register-commands.js new file mode 100644 index 0000000..456de80 --- /dev/null +++ b/integrations/discord-bot/register-commands.js @@ -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."); diff --git a/public/index.html b/public/index.html index 18e8281..189a924 100644 --- a/public/index.html +++ b/public/index.html @@ -2,120 +2,49 @@
+