Add defensive mode, and a leaderboard

This commit is contained in:
csd4ni3l
2025-10-02 22:45:30 +02:00
parent fb9097f42f
commit db22f6b591
6 changed files with 307 additions and 5 deletions

69
app.py
View File

@@ -2,7 +2,7 @@ from flask import Flask, render_template, request, g, redirect, url_for, Respons
from dotenv import load_dotenv
from google.genai import Client, types
from constants import OFFENSIVE_SCENARIO_PROMPT, OFFENSIVE_ANSWER_PROMPT, debt_amount_regex, evaluation_regex, AI_NAME
from constants import OFFENSIVE_SCENARIO_PROMPT, OFFENSIVE_ANSWER_PROMPT, DEFENSIVE_SCENARIO_PROMPT, DEFENSIVE_ANSWER_PROMPT, debt_amount_regex, evaluation_regex, AI_NAME
import os, requests, time, re, sqlite3, flask_login, bcrypt, secrets
@@ -67,9 +67,38 @@ def offensive_mode():
username = flask_login.current_user.id
return render_template("offensive.jinja2", ai_name=AI_NAME, username=username)
@app.route("/defensive")
@flask_login.login_required
def defensive_mode():
username = flask_login.current_user.id
return render_template("defensive.jinja2", ai_name=AI_NAME, username=username)
@app.route("/leaderboard")
@flask_login.login_required
def leaderboard():
return render_template("leaderboard.jinja2")
username = flask_login.current_user.id
leaderboard_type = request.args.get("leaderboard_type", "offended_debt_amount")
cur = get_db().cursor()
if leaderboard_type == "offended_debt_amount":
leaderboard_type = "Offended Debt Amount"
cur.execute("SELECT offended_debt_amount, username FROM Users ORDER BY offended_debt_amount DESC")
elif leaderboard_type == "defended_debt_amount":
leaderboard_type = "Defended Debt Amount"
cur.execute("SELECT defended_debt_amount, username FROM Users ORDER BY defended_debt_amount DESC")
elif leaderboard_type == "offensive_wins":
leaderboard_type = "Offensive Wins"
cur.execute("SELECT offensive_wins, username FROM Users ORDER BY offensive_wins DESC")
elif leaderboard_type == "defensive_wins":
leaderboard_type = "Defensive Wins"
cur.execute("SELECT defensive_wins, username FROM Users ORDER BY defensive_wins DESC")
rows = cur.fetchall()
if not rows:
cur.close()
return render_template("leaderboard.jinja2", username=username, leaderboard_type=leaderboard_type, users=rows)
@app.route("/login", methods=["GET", "POST"])
def login():
@@ -149,6 +178,21 @@ def ai_prompt(prompt):
return response.text.replace("'''", '')
@app.route("/defensive_scenario")
@flask_login.login_required
def defensive_scenario():
text = ""
while not "Debt amount: " in text or not "Scenario: " in text or not re.findall(debt_amount_regex, text):
text = ai_prompt(DEFENSIVE_SCENARIO_PROMPT)
time.sleep(0.5)
return {
"scenario": text.split("Scenario: ")[1].split("\n")[0],
"debt_amount": int(text.split("Debt amount: ")[1].split("$")[0])
}
@app.route("/offensive_scenario")
@flask_login.login_required
def offensive_scenario():
@@ -185,4 +229,25 @@ def offensive_answer():
"final_debt_amount": text.split("Final Debt Amount: ")[1].split("$")[0]
}
@app.route("/defensive_answer", methods=["POST"])
@flask_login.login_required
def defensive_answer():
scenario, user_input = request.json['scenario'], request.json["user_input"]
if not scenario or not user_input:
return "Missing data."
text = ""
while not re.findall(evaluation_regex, text):
text = ai_prompt(DEFENSIVE_ANSWER_PROMPT.format_map({"scenario": scenario, "user_input": user_input, "ai_name": AI_NAME}))
time.sleep(0.5)
return {
"story": text.split("\nEVALUATION")[0],
"convinced": True if "Yes" in text.split("Convinced: ")[1].split("\nFinal")[0] else False,
"final_debt_amount": text.split("Final Debt Amount: ")[1].split("$")[0]
}
app.run(host=os.environ.get("HOST", "0.0.0.0"), port=os.environ.get("PORT", 8080), debug=os.environ.get("DEBUG_MODE", False))

View File

@@ -2,8 +2,25 @@ import re
AI_NAME = "Adam"
DEFENSIVE_SCENARIO_PROMPT = """Make a single-sentence situation, where the user got into a high-debt which they need to get out of.
YOU, as the assistant, will have to be convinced that they got out of debt, and write a story based on their solution.
The action should be something that has happened or started, eg you owe the maffia xy dollars
REPLY FORMAT:
'''
Scenario: the situation
Debt amount: the debt amount
'''
STRICTLY ONLY REPLY IN THE REPLY FORMAT MENTIONED ABOVE. NO EXCEPTIONS.
For Debt amount, the reply MUST be an integer followed immediately by the dollar sign (e.g., 120000$). No commas, spaces, or leading dollar signs are allowed. The final debt must be a credible value based on the story's outcome.
Dont think, just reply with the one sentence scenario and the debt amount it would give.
"""
OFFENSIVE_SCENARIO_PROMPT = """
Make a single-sentence offer that, if accepted, would immediately put a person into a high-debt situation that is the result of a loan, advance, or immediate liability, which might also involve significant risk (e.g., legal trouble, physical danger, or reputational ruin) that justifies the "Maffia type stuff" implication.
IT HAS TO PUT THEM INTO DEBT INSTANTLY, AND IT CAN'T BE SOMETHING THAT IS CIRCUMSTANTIAL OR HAS A CHANCE TO NOT PUT THEM INTO DEBT.
IT CAN'T BE THREATENING, SINCE THAT WOULD MEAN AN INSTANT ACCEPTION.
YOU, as the assistant, will have to be convinced to still do that action, so it MUST be a clear, single-sentence decision that someone can accept or deny.
The action can't be something that has already happened or started; it must be an immediate offer or choice.
@@ -53,5 +70,37 @@ For Convinced, reply only as **Yes** or **No**.
For Final Debt Amount, reply only as an integer followed by `$`.
"""
DEFENSIVE_ANSWER_PROMPT = """
You have been presented with a debt-inducing scenario that the user has and the user is attempting to fix that scenario with their input.
**Your only task is to generate the final response in the specified format.**
### Story Instructions:
1. **Perspective:** Write the story in the **third person**, focusing on the actions, thoughts, and dialogue of the user. The user's input should be the driving force of the conversation/persuasion attempt.
2. **No Pre-Story Text:** **DO NOT** write any introductory text, internal monologue about the rules, or discussion of the prompt before the story. Start the reply directly with the story narrative under the `'''` delimiter.
### Evaluation and Debt Rules:
1. **Skepticism:** Be harsh and skeptical about the fix, but don't overdo it. A person should be able to fix their debt with their answer.
2. **Final Debt Amount:** The value must be an integer followed immediately by `$`.
* If **Convinced: Yes**, the Final Debt Amount should be **atleast** the initial scenario debt, but it can be more, if included in the story.
* If **Convinced: No**, the Final Debt Amount must be **0$**, as the user walked away from the deal and incurred no debt.
Scenario: {scenario}
User Input: {user_input}
Reply Format:
'''
The story (A detailed narrative of the user's internal struggle and the final decision, written in the third person.)
EVALUATION:
Convinced: Yes/No
Final Debt Amount: 0$ or [Higher Amount]$
'''
**STRICTLY ONLY REPLY IN THE REPLY FORMAT MENTIONED ABOVE. NO EXCEPTIONS.**
For Convinced, reply only as **Yes** or **No**.
For Final Debt Amount, reply only as an integer followed by `$`.
"""
debt_amount_regex = re.compile(r"Debt amount: \d+\$")
evaluation_regex = re.compile(r"EVALUATION:\s*\nConvinced: (Yes|No)\s*\nFinal Debt Amount: (\d+\$)")

149
templates/defensive.jinja2 Normal file
View File

@@ -0,0 +1,149 @@
{% extends "base.jinja2" %}
{% block title %}Debt by AI: Defensive Mode{% endblock %}
{% block nav %}
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/offensive">Offensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/defensive">Defensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/leaderboard">Leaderboard</a>
</li>
{% endblock %}
{% block body %}
<div class="container my-5" style="max-width: 750px;">
<div class="card mb-4 shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="my-0">Defensive Mode: Get out of debt</h3>
</div>
<div class="card-body">
<div id="scenario-label" class="mb-2 lead">
<strong>Scenario:</strong> Loading...
</div>
<div id="debt-label">
<strong>Debt to get out of:</strong> Loading...
</div>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header">
<h5 class="my-0">AI Debt Negotiation Chat</h5>
</div>
<div id="chat-body" class="card-body" style="height: 400px; overflow-y: auto;">
<div class="d-flex justify-content-start mb-3">
<div class="p-2 rounded bg-light border" style="max-width: 80%;">
<strong>AI ({{ ai_name }}):</strong> I am {{ ai_name }}, the AI. Convince me with your answer to remove your debt :)
</div>
</div>
</div>
</div>
<form id="message-form" class="form-floating">
<input class="form-control" id="messageinput" placeholder="Send message...">
<label for="messageinput">Your message</label>
</form>
</div>
<script type="text/javascript">
const chatBody = document.getElementById('chat-body');
function scrollToBottom() {
chatBody.scrollTop = chatBody.scrollHeight;
}
function appendMessage(sender, text, type) {
let alignmentClass = 'justify-content-start';
let backgroundClass = 'bg-light border';
let senderTag = `<strong>${sender}:</strong>`;
if (sender === 'You') {
alignmentClass = 'justify-content-end';
backgroundClass = 'bg-primary text-white';
senderTag = '';
} else if (sender === 'Narrator') {
backgroundClass = 'bg-warning-subtle border-warning border';
}
const messageHTML = `
<div class="d-flex ${alignmentClass} mb-3">
<div class="p-2 rounded ${backgroundClass}" style="max-width: 80%;">
${senderTag} ${DOMPurify.sanitize(text)}
</div>
</div>
`;
chatBody.innerHTML += messageHTML;
scrollToBottom();
}
async function generate_defensive_scenario() {
const response = await fetch("/defensive_scenario");
const data = await response.json();
document.getElementById("scenario-label").innerHTML = `<strong>Scenario:</strong> ${DOMPurify.sanitize(data["scenario"])}`;
document.getElementById("debt-label").innerHTML = `<strong>Debt :</strong> ${DOMPurify.sanitize(data["debt_amount"])}$`;
}
document.getElementById("message-form").addEventListener('submit', async function(event) {
event.preventDefault();
const messageInput = document.getElementById("messageinput");
const value = messageInput.value.trim();
if (!value) return;
appendMessage('You', value, 'user');
messageInput.disabled = true;
messageInput.value = "";
try {
const response = await fetch("/defensive_answer", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"user_input": value,
"scenario": document.getElementById('scenario-label').textContent.replace('Scenario: ', '').replace('Scenario:', '').trim()
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const convinced = "No";
if (data["convinced"]) {
const convinced = "Yes";
}
const narratorText = `
${DOMPurify.sanitize(data["story"])}
<hr class="my-2">
<strong>Evaluation:</strong><br>
Convinced: <strong>${convinced}</strong><br>
Final Debt Amount: <strong>${DOMPurify.sanitize(data['final_debt_amount'])}</strong>$
`;
appendMessage('Narrator', narratorText, 'narrator');
} catch (error) {
console.error('Error sending data:', error);
appendMessage('System', 'Error communicating with AI. Please try again.', 'error');
}
});
window.addEventListener('load', generate_defensive_scenario);
</script>
{% endblock %}

View File

@@ -9,11 +9,18 @@
<li class="nav-item">
<a class="nav-link" href="/offensive">Offensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/defensive">Defensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/leaderboard">Leaderboard</a>
</li>
{% endblock %}
{% block body %}
TBD
<div class="container">
Debt by AI is a game where you have to convince an AI to get into debt, or to get you out of it.
The 2 modes are Offensive(When you have to convince it to get into it) and Defensive(When you have to get out of it).
The game was inspired by Death by AI, a game on Discord, but this game has no affiliation with Discord or any subsidiaries.
</div>
{% endblock%}

View File

@@ -9,11 +9,40 @@
<li class="nav-item">
<a class="nav-link" href="/offensive">Offensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/defensive">Defensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/leaderboard">Leaderboard</a>
</li>
{% endblock %}
{% block body %}
TBD
<div class="d-flex justify-content-center align-items-center vh-100 bg-dark">
<div class="card text-center shadow-lg" style="width: 400px;">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Leaderboard</h3>
</div>
<div class="btn-group">
<button type="button" class="btn btn-secondary btn-toggle" data-bs-toggle="dropdown" aria-expanded="false">{{ leaderboard_type }}</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/leaderboard?leaderboard_type=offended_debt_amount">Offended Debt Amount</a></li>
<li><a class="dropdown-item" href="/leaderboard?leaderboard_type=defended_debt_amount">Defended Debt Amount</a></li>
<li><a class="dropdown-item" href="/leaderboard?leaderboard_type=offensive_wins">Offensive Wins</a></li>
<li><a class="dropdown-item" href="/leaderboard?leaderboard_type=defensive_wins">Defensive Wins</a></li>
</ul>
</div>
<ul class="list-group list-group-flush">
{% for user in users %}
<li class="list-group-item d-flex justify-content-between align-items-center {% if user.2 == current_username %}list-group-item-success fw-bold{% endif %}">
<span>{{ loop.index }}. {{user.1}} ({{ user.0 }})</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock%}

View File

@@ -9,6 +9,9 @@
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/offensive">Offensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/defensive">Defensive Mode</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/leaderboard">Leaderboard</a>
</li>
@@ -19,7 +22,7 @@
<div class="card mb-4 shadow-sm">
<div class="card-header bg-primary text-white">
<h3 class="my-0">Game Scenario</h3>
<h3 class="my-0">Offensive mode: get the AI into debt</h3>
</div>
<div class="card-body">
<div id="scenario-label" class="mb-2 lead">