mirror of
https://github.com/csd4ni3l/debt-by-ai.git
synced 2026-01-01 04:23:45 +01:00
Add defensive mode, and a leaderboard
This commit is contained in:
69
app.py
69
app.py
@@ -2,7 +2,7 @@ from flask import Flask, render_template, request, g, redirect, url_for, Respons
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from google.genai import Client, types
|
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
|
import os, requests, time, re, sqlite3, flask_login, bcrypt, secrets
|
||||||
|
|
||||||
@@ -67,9 +67,38 @@ def offensive_mode():
|
|||||||
username = flask_login.current_user.id
|
username = flask_login.current_user.id
|
||||||
return render_template("offensive.jinja2", ai_name=AI_NAME, username=username)
|
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")
|
@app.route("/leaderboard")
|
||||||
|
@flask_login.login_required
|
||||||
def leaderboard():
|
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"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
@@ -149,6 +178,21 @@ def ai_prompt(prompt):
|
|||||||
|
|
||||||
return response.text.replace("'''", '')
|
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")
|
@app.route("/offensive_scenario")
|
||||||
@flask_login.login_required
|
@flask_login.login_required
|
||||||
def offensive_scenario():
|
def offensive_scenario():
|
||||||
@@ -185,4 +229,25 @@ def offensive_answer():
|
|||||||
"final_debt_amount": text.split("Final Debt Amount: ")[1].split("$")[0]
|
"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))
|
app.run(host=os.environ.get("HOST", "0.0.0.0"), port=os.environ.get("PORT", 8080), debug=os.environ.get("DEBUG_MODE", False))
|
||||||
49
constants.py
49
constants.py
@@ -2,8 +2,25 @@ import re
|
|||||||
|
|
||||||
AI_NAME = "Adam"
|
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 = """
|
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.
|
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.
|
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.
|
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 `$`.
|
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+\$")
|
debt_amount_regex = re.compile(r"Debt amount: \d+\$")
|
||||||
evaluation_regex = re.compile(r"EVALUATION:\s*\nConvinced: (Yes|No)\s*\nFinal Debt Amount: (\d+\$)")
|
evaluation_regex = re.compile(r"EVALUATION:\s*\nConvinced: (Yes|No)\s*\nFinal Debt Amount: (\d+\$)")
|
||||||
149
templates/defensive.jinja2
Normal file
149
templates/defensive.jinja2
Normal 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 %}
|
||||||
@@ -9,11 +9,18 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/offensive">Offensive Mode</a>
|
<a class="nav-link" href="/offensive">Offensive Mode</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/defensive">Defensive Mode</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/leaderboard">Leaderboard</a>
|
<a class="nav-link" href="/leaderboard">Leaderboard</a>
|
||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% 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%}
|
{% endblock%}
|
||||||
@@ -9,11 +9,40 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/offensive">Offensive Mode</a>
|
<a class="nav-link" href="/offensive">Offensive Mode</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/defensive">Defensive Mode</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="/leaderboard">Leaderboard</a>
|
<a class="nav-link active" aria-current="page" href="/leaderboard">Leaderboard</a>
|
||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% 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%}
|
{% endblock%}
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="/offensive">Offensive Mode</a>
|
<a class="nav-link active" aria-current="page" href="/offensive">Offensive Mode</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/defensive">Defensive Mode</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/leaderboard">Leaderboard</a>
|
<a class="nav-link" href="/leaderboard">Leaderboard</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
|
|
||||||
<div class="card mb-4 shadow-sm">
|
<div class="card mb-4 shadow-sm">
|
||||||
<div class="card-header bg-primary text-white">
|
<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>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="scenario-label" class="mb-2 lead">
|
<div id="scenario-label" class="mb-2 lead">
|
||||||
|
|||||||
Reference in New Issue
Block a user