Compare commits

3 Commits

Author SHA1 Message Date
Keith Solomon
428d111cf7 🐞 fix: Update functions to work, make new game button stay inline 2025-08-15 18:51:11 -05:00
Keith Solomon
17e925ec8e feat: Add detailed PHPDoc comments and improve code readability 2025-08-15 18:12:19 -05:00
Keith Solomon
b9cb61750d feat: Implement SQLite backend and configuration UI 2025-08-15 07:08:26 -05:00
10 changed files with 445 additions and 84 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
game.db

View File

@@ -10,22 +10,30 @@ Each turn, you roll the dice to see what happens in the city. The events will af
You can also try to assassinate the emperor. This is a risky move, but if you succeed, you win the game (though you die a martyr). You can also try to assassinate the emperor. This is a risky move, but if you succeed, you win the game (though you die a martyr).
## Configuration
This project uses a SQLite database to store game data and configuration.
You can modify the game's configuration by clicking on the "Configuration" button on the main page. This will take you to a page where you can edit the score thresholds, win conditions, and event descriptions.
## Technical Details ## Technical Details
This project is built using: This project is built using:
* **Frontend:** HTML, CSS, JavaScript * **Frontend:** HTML, CSS, JavaScript
* **Backend:** PHP * **Backend:** PHP
* **Database:** SQLite
## How to Run Locally ## How to Run Locally
To run this project locally, you need a PHP server. You can use a local development environment like XAMPP or MAMP, or you can use the built-in PHP server. To run this project locally, you need a PHP server with the `pdo_sqlite` extension enabled. You can use a local development environment like XAMPP or MAMP, or you can use the built-in PHP server.
1. Clone this repository. 1. Clone this repository.
2. Start a PHP server in the project directory. For example, you can run the following command: 2. Make sure the `pdo_sqlite` extension is enabled in your `php.ini` file.
3. Start a PHP server in the project directory. For example, you can run the following command:
```bash ```bash
php -S localhost:8000 php -S localhost:8000
``` ```
3. Open your web browser and go to `http://localhost:8000`. 4. Open your web browser and go to `http://localhost:8000`.

24
config.css Normal file
View File

@@ -0,0 +1,24 @@
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.form-group textarea {
height: 100px;
}
button[type="submit"] {
margin-top: 1rem;
}

74
config.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
/**
* Config file for the Last Days of Rome.
*
* PHP version: 8.0+
*
* @category Configuration
* @package Rome
* @author Keith Solomon <keith@keithsolmon.net>
* @license MIT License
* @version GIT: $Id$
* @link https://git.keithsolomon.net/keith/Warframe_Shopping_List
*/
require_once 'database.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($_POST as $name => $value) {
$stmt = $db->prepare('UPDATE configuration SET value = ? WHERE name = ?');
$stmt->execute([$value, $name]);
}
header('Location: config.php');
exit;
}
$stmt = $db->query('SELECT name, value FROM configuration');
$config = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration - Last Days of Rome</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="config.css">
</head>
<body>
<div class="container">
<h1>Configuration</h1>
<form method="POST">
<?php foreach ($config as $name => $value): ?>
<div class="form-group">
<label for="<?php echo $name ?>">
<?php echo ucwords(str_replace('_', ' ', $name)) ?>
</label>
<?php if (strlen($value) > 80) : ?>
<textarea
id="<?php echo $name ?>"
name="<?php echo $name ?>"
>
<?php echo htmlspecialchars($value) ?>
</textarea>
<?php else: ?>
<input
type="text"
id="<?php echo $name ?>"
name="<?php echo $name ?>"
value="<?php echo htmlspecialchars($value) ?>"
>
<?php endif; ?>
</div>
<?php endforeach; ?>
<button type="submit">Save Configuration</button>
</form>
<a href="index.php">Back to Game</a>
</div>
</body>
</html>

111
database.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
/**
* Database configuration for the Last Days of Rome.
*
* PHP version: 8.0+
*
* @category Database
* @package Rome
* @author Keith Solomon <keith@keithsolmon.net>
* @license MIT License
* @version GIT: $Id$
* @link https://git.keithsolomon.net/keith/Warframe_Shopping_List
*/
$dbFile = 'game.db';
if (!file_exists($dbFile)) {
touch($dbFile);
}
$db = new PDO('sqlite:' . $dbFile);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create tables if they don't exist
$db->exec(
'CREATE TABLE IF NOT EXISTS game_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_played DATETIME,
flames_score INTEGER,
desolation_score INTEGER,
relocation_score INTEGER,
end_condition TEXT
)'
);
$db->exec(
'CREATE TABLE IF NOT EXISTS configuration (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
value TEXT
)'
);
// Check if configuration is already seeded
$stmt = $db->query('SELECT COUNT(*) FROM configuration');
$count = $stmt->fetchColumn();
if ($count === 0) {
// Seed the configuration table
$config = [
['flames_win', '10'],
['desolation_win', '10'],
['relocation_win', '10'],
['assassination_win', '20'],
[
'imperial_palace_event_1',
'The emperor fiddles with manic glee. +1 Flames.'
],
[
'imperial_palace_event_2',
'The emperor raises another horse to the position of senator. +1 Desolation, +1 Relocation.'
],
[
'imperial_palace_event_3',
'It\'s execution night at the palace. +1 Relocation.'
],
[
'imperial_palace_event_4',
'The emperor screams like a baby. +1 Desolation.'
],
[
'imperial_palace_event_5',
'The emperor sits in front of the flame and commands it to obey. It does not. +1 Flames, +1 Relocation.'
],
[
'imperial_palace_event_6',
'Work continues on a house made of pure gold. It keeps melting. +1 Flames.'
],
[
'relative_unrest_event_1',
'People complain - this is unacceptable. Then they go about their business. +1 Relocation.'
],
[
'relative_unrest_event_2',
'There are no goods at market. +1 Desolation.'
],
[
'relative_unrest_event_3',
'You receive a letter asking you to reassert your faith in the emperor. In writing. +1 Relocation.'
],
[
'relative_unrest_event_4',
'Lions are released onto the streets. +1 Desolation, +1 Flames.'
],
[
'relative_unrest_event_5',
'Is Rome really over? +1 Desolation.'
],
[
'relative_unrest_event_6',
'The burnings will continue until morale improves. +1 Flames.'
],
];
$stmt = $db->prepare('INSERT INTO configuration (name, value) VALUES (?, ?)');
foreach ($config as $item) {
$stmt->execute($item);
}
}

275
game.php
View File

@@ -1,6 +1,25 @@
<?php <?php
/**
* Config file for the Last Days of Rome.
*
* PHP version: 8.0+
*
* @category API
* @package Rome
* @author Keith Solomon <keith@keithsolmon.net>
* @license MIT License
* @version GIT: $Id$
* @link https://git.keithsolomon.net/keith/Warframe_Shopping_List
*/
session_start(); session_start();
require_once 'database.php';
// Fetch configuration from the database
$stmt = $db->query('SELECT name, value FROM configuration');
$config = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
if (!isset($_SESSION['scores'])) { if (!isset($_SESSION['scores'])) {
$_SESSION['scores'] = [ $_SESSION['scores'] = [
'flames' => 0, 'flames' => 0,
@@ -12,135 +31,233 @@ if (!isset($_SESSION['scores'])) {
$action = $_GET['action'] ?? ''; $action = $_GET['action'] ?? '';
if ($action === 'roll_dice') { if ($action === 'roll_dice') {
$event = roll_dice(); $event = rollDice($config);
$gameState = check_game_state(); $gameState = checkGameState($config);
echo json_encode([
'scores' => $_SESSION['scores'], if ($gameState !== 'ongoing') {
'event' => $event, logGame($gameState);
'gameState' => $gameState, }
]);
echo json_encode(
[
'scores' => $_SESSION['scores'],
'event' => $event,
'gameState' => $gameState,
]
);
} elseif ($action === 'assassinate') { } elseif ($action === 'assassinate') {
$assassinationResult = assassinate(); $assassinationResult = assassinate($config);
echo json_encode([
'event' => $assassinationResult['event'], if ($assassinationResult['gameState'] !== 'ongoing') {
'gameState' => $assassinationResult['gameState'], logGame($assassinationResult['gameState']);
]); }
echo json_encode(
[
'event' => $assassinationResult['event'],
'gameState' => $assassinationResult['gameState'],
]
);
} elseif ($action === 'reset_game') { } elseif ($action === 'reset_game') {
$_SESSION['scores'] = [ $_SESSION['scores'] = [
'flames' => 0, 'flames' => 0,
'desolation' => 0, 'desolation' => 0,
'relocation' => 0, 'relocation' => 0,
]; ];
echo json_encode([
'scores' => $_SESSION['scores'], echo json_encode(
]); [
'scores' => $_SESSION['scores'],
]
);
} }
function roll_dice() { /**
* Rolls a dice and determines the event based on the result.
*
* @param array $config The game configuration.
*
* @return string The event description.
*/
function rollDice($config) {
$roll = rand(1, 6); $roll = rand(1, 6);
$event = ''; $event = '';
if ($roll <= 3) { if ($roll <= 3) {
$event = 'In the Imperial Palace: ' . imperial_palace_event(); $event = 'In the Imperial Palace: ' . imperialPalaceEvent($config);
} elseif ($roll <= 5) { } elseif ($roll <= 5) {
$event = 'Relative Unrest: ' . relative_unrest_event(); $event = 'Relative Unrest: ' . relativeUnrestEvent($config);
} else { } else {
$_SESSION['scores']['flames']++; $_SESSION['scores']['flames']++;
$event = 'The fires spread. +1 Flames.'; $event = 'The fires spread. +1 Flames.';
} }
return $event; return $event;
} }
function imperial_palace_event() { /**
* Handles events occurring in the Imperial Palace based on a dice roll.
*
* @param array $config The game configuration.
*
* @return string The event description.
*/
function imperialPalaceEvent($config) {
$roll = rand(1, 6); $roll = rand(1, 6);
$event = ''; $event = '';
switch ($roll) { switch ($roll) {
case 1: case 1:
$_SESSION['scores']['flames']++; $_SESSION['scores']['flames']++;
$event = 'The emperor fiddles with manic glee. +1 Flames.'; $event = $config['imperial_palace_event_1'];
break; break;
case 2: case 2:
$_SESSION['scores']['desolation']++; $_SESSION['scores']['desolation']++;
$_SESSION['scores']['relocation']++; $_SESSION['scores']['relocation']++;
$event = 'The emperor raises another horse to the position of senator. +1 Desolation, +1 Relocation.'; $event = $config['imperial_palace_event_2'];
break; break;
case 3: case 3:
$_SESSION['scores']['relocation']++; $_SESSION['scores']['relocation']++;
$event = 'It\'s execution night at the palace. +1 Relocation.'; $event = $config['imperial_palace_event_3'];
break; break;
case 4: case 4:
$_SESSION['scores']['desolation']++; $_SESSION['scores']['desolation']++;
$event = 'The emperor screams like a baby. +1 Desolation.'; $event = $config['imperial_palace_event_4'];
break; break;
case 5: case 5:
$_SESSION['scores']['flames']++; $_SESSION['scores']['flames']++;
$_SESSION['scores']['relocation']++; $_SESSION['scores']['relocation']++;
$event = 'The emperor sits in front of the flame and commands it to obey. It does not. +1 Flames, +1 Relocation.'; $event = $config['imperial_palace_event_5'];
break; break;
case 6: case 6:
$_SESSION['scores']['flames']++; $_SESSION['scores']['flames']++;
$event = 'Work continues on a house made of pure gold. It keeps melting. +1 Flames.'; $event = $config['imperial_palace_event_6'];
break; break;
} }
return $event; return $event;
} }
function relative_unrest_event() { /**
* Handles events related to relative unrest based on a dice roll.
*
* @param array $config The game configuration.
*
* @return string The event description.
*/
function relativeUnrestEvent($config) {
$roll = rand(1, 6); $roll = rand(1, 6);
$event = ''; $event = '';
switch ($roll) { switch ($roll) {
case 1: case 1:
$_SESSION['scores']['relocation']++; $_SESSION['scores']['relocation']++;
$event = 'People complain - this is unacceptable. Then they go about their business. +1 Relocation.'; $event = $config['relative_unrest_event_1'];
break; break;
case 2: case 2:
$_SESSION['scores']['desolation']++; $_SESSION['scores']['desolation']++;
$event = 'There are no goods at market. +1 Desolation.'; $event = $config['relative_unrest_event_2'];
break; break;
case 3: case 3:
$_SESSION['scores']['relocation']++; $_SESSION['scores']['relocation']++;
$event = 'You receive a letter asking you to reassert your faith in the emperor. In writing. +1 Relocation.'; $event = $config['relative_unrest_event_3'];
break; break;
case 4: case 4:
$_SESSION['scores']['desolation']++; $_SESSION['scores']['desolation']++;
$_SESSION['scores']['flames']++; $_SESSION['scores']['flames']++;
$event = 'Lions are released onto the streets. +1 Desolation, +1 Flames.'; $event = $config['relative_unrest_event_4'];
break; break;
case 5: case 5:
$_SESSION['scores']['desolation']++; $_SESSION['scores']['desolation']++;
$event = 'Is Rome really over? +1 Desolation.'; $event = $config['relative_unrest_event_5'];
break; break;
case 6: case 6:
$_SESSION['scores']['flames']++; $_SESSION['scores']['flames']++;
$event = 'The burnings will continue until morale improves. +1 Flames.'; $event = $config['relative_unrest_event_6'];
break; break;
} }
return $event; return $event;
} }
function assassinate() { /**
* Attempts to assassinate the emperor based on the current desolation score.
*
* @param array $config The game configuration.
*
* @return array An array containing the event description and game state.
*/
function assassinate($config) {
$desolation = $_SESSION['scores']['desolation']; $desolation = $_SESSION['scores']['desolation'];
$total = 0; $total = 0;
for ($i = 0; $i < $desolation; $i++) { for ($i = 0; $i < $desolation; $i++) {
$total += rand(1, 6); $total += rand(1, 6);
} }
if ($total >= 20) { if ($total >= $config['assassination_win']) {
return ['event' => 'You have assassinated the emperor! You die a martyr.', 'gameState' => 'win_assassination']; return [
'event' => 'You have assassinated the emperor! You die a martyr.',
'gameState' => 'win_assassination'
];
} else { } else {
return ['event' => 'Your attempt to assassinate the emperor has failed. You are executed.', 'gameState' => 'loss_assassination']; return [
'event' => 'Your attempt to assassinate the emperor has failed. You are executed.',
'gameState' => 'loss_assassination'
];
} }
} }
function check_game_state() { /**
if ($_SESSION['scores']['flames'] >= 10) { * Checks the current game state based on scores and configuration.
*
* @param array $config The game configuration.
*
* @return string The current game state.
*/
function checkGameState($config) {
if ($_SESSION['scores']['flames'] >= $config['flames_win']) {
return 'loss_flames'; return 'loss_flames';
} }
if ($_SESSION['scores']['desolation'] >= 10) {
if ($_SESSION['scores']['desolation'] >= $config['desolation_win']) {
return 'loss_desolation'; return 'loss_desolation';
} }
if ($_SESSION['scores']['relocation'] >= 10) {
if ($_SESSION['scores']['relocation'] >= $config['relocation_win']) {
return 'win'; return 'win';
} }
return 'ongoing'; return 'ongoing';
} }
/**
* Logs the end of a game with scores and end condition.
*
* @param string $endCondition The condition that ended the game.
*
* @return void
*/
function logGame($endCondition) {
global $db;
$stmt = $db->prepare(
'INSERT INTO game_logs (
date_played,
flames_score,
desolation_score,
relocation_score,
end_condition)
VALUES (?, ?, ?, ?, ?)'
);
$stmt->execute(
[
date('Y-m-d H:i:s'),
$_SESSION['scores']['flames'],
$_SESSION['scores']['desolation'],
$_SESSION['scores']['relocation'],
$endCondition
]
);
}

8
gemini.md Normal file
View File

@@ -0,0 +1,8 @@
Make the following updates to the project and update the readme file as needed for documentation.
Add a sqlite backend for the following:
- Game logs: Track date and time played, final scores, and end condition (escape, loss, assassination outcome, etc).
- Configuration: Score thresholds, win conditions, event descriptions, etc.
- Default configuration should be provided using current hardcoded values.
- Allow user to modify configuration via a simple web form.
- Store configuration changes in the database.

View File

@@ -19,6 +19,7 @@
<button id="roll-button">Roll the Dice</button> <button id="roll-button">Roll the Dice</button>
<button id="assassinate-button">Try to Assassinate the Emperor</button> <button id="assassinate-button">Try to Assassinate the Emperor</button>
<button id="new-game-button" style="display: none;">New Game</button> <button id="new-game-button" style="display: none;">New Game</button>
<a href="config.php" class="button">Configuration</a>
</div> </div>
<div class="intro"> <div class="intro">

View File

@@ -71,7 +71,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (gameState !== 'ongoing') { if (gameState !== 'ongoing') {
rollButton.disabled = true; rollButton.disabled = true;
assassinateButton.disabled = true; assassinateButton.disabled = true;
newGameButton.style.display = 'block'; newGameButton.style.display = 'inline-block';
let message = ''; let message = '';
if (gameState === 'win') { if (gameState === 'win') {
message = 'You have escaped the city! You win!'; message = 'You have escaped the city! You win!';
@@ -87,4 +87,4 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => alert(message), 100); setTimeout(() => alert(message), 100);
} }
} }
}); });

View File

@@ -77,6 +77,23 @@ h1 { color: var(--h1-color-light); }
&:hover { background-color: var(--button-hover-background-color); } &:hover { background-color: var(--button-hover-background-color); }
} }
.actions a.button {
background-color: var(--button-background-color);
border: none;
color: var(--button-text-color);
cursor: pointer;
display: inline-block;
font-size: 1em;
margin: 0 .5rem 1.5rem;
padding: .5rem 1.5rem;
text-decoration: none;
transition: background-color 0.3s;
}
.actions a.button:hover {
background-color: var(--button-hover-background-color);
}
.intro { .intro {
font-size: 1.1rem; font-size: 1.1rem;
line-height: 1.4; line-height: 1.4;