Compare commits

...

7 Commits

Author SHA1 Message Date
j 08851d70fc implemented save and load of game state 2024-03-26 22:55:04 -04:00
j 5e898fe8ce game state db 2024-03-26 22:54:15 -04:00
j f5698d37e1 fixed a typo 2024-03-26 22:53:26 -04:00
j dac5f3b361 cleanup 2024-03-26 22:53:15 -04:00
j d1aa19e4c3 updated database service 2024-03-26 22:53:00 -04:00
j 0087c3523d added handling for mountains 2024-03-26 22:50:32 -04:00
j 01a2825fc0 potential input handling 2024-03-25 23:59:01 -04:00
8 changed files with 188 additions and 185 deletions

View File

@ -490,7 +490,7 @@
4690 GOTO 4710
4700 REM ***MOUNTAINS***
4710 IF H <= 950 THEN 1230
4710 IF totalMileageWholeTrip <= 950 THEN 1230
4720 IF RND(-1)*10>9-((totalMileageWholeTrip/100-15)^2+72)/((totalMileageWholeTrip/100-15)^2+12) THEN 4860
4730 PRINT "RUGGED MOUNTAINS"
4740 IF RND(-1)>.1 THEN 4780

View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS gameState (
authorId INTEGER PRIMARY KEY,
data TEXT NOT NULL
);

View File

@ -1,28 +0,0 @@
CREATE TABLE IF NOT EXISTS user_mentions (
-- Unique identifier for each record. Auto-incremented to ensure uniqueness.
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- The unique identifier of the comment from the r/Drama platform. Not unique in this table
-- because a single comment can mention multiple users.
rdrama_comment_id TEXT NOT NULL,
-- The mentioned Reddit username in a standardized format (e.g., u/username). Lowercased
-- to ensure consistency and prevent duplicate entries due to case differences.
username TEXT NOT NULL,
-- The content of the message sent to the mentioned user, if any. Allows tracking
-- of what communication has been made, useful for audit purposes or resending messages.
message TEXT,
-- Timestamp when the mention was processed and, if applicable, when a message was sent.
-- Defaults to the current timestamp at the time of record creation.
sent_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Enforces uniqueness for each comment-username pair to prevent processing and notifying
-- the same user mention in the same comment more than once.
CONSTRAINT unique_comment_user UNIQUE (rdrama_comment_id, username)
);
-- Consider adding indexes based on query patterns for improved performance, such as:
-- CREATE INDEX idx_username ON user_mentions(username);
-- CREATE INDEX idx_rdrama_comment_id ON user_mentions(rdrama_comment_id);

View File

@ -1,9 +0,0 @@
CREATE TABLE IF NOT EXISTS oauth_tokens (
id INTEGER PRIMARY KEY,
token_identifier TEXT NOT NULL UNIQUE, -- Static identifier for the OAuth token
access_token TEXT NOT NULL,
token_type TEXT NOT NULL,
expires_in INTEGER NOT NULL,
expiry_timestamp INTEGER NOT NULL,
scope TEXT NOT NULL
);

View File

@ -1,6 +1,6 @@
import { Database } from 'sqlite';
import { Comment } from '../../rdrama/models/Comment';
import { DatabaseInitializer } from '../initializeDatabase';
import GameState from '../../game/gameState';
/**
* Service for interacting with the SQLite database for operations related to comments and user mentions.
@ -27,136 +27,56 @@ export class DatabaseService {
}
/**
* Inserts a new user mention into the database.
* This static method adds a record of a user being mentioned in a comment.
*
* Loads an existing game state for a given author from the database.
* If an existing game state is found, it returns a new GameState instance initialized with the stored values.
* If no game state exists for the given author, it returns null, indicating that a new game state needs to be initialized.
*
* @example
* await DatabaseService.insertUserMention({
* rdrama_comment_id: 456,
* username: 'mentionedUser',
* message: 'You were mentioned in a comment.'
* });
*
* @param {Object} mention - The user mention object to insert.
* @param {number} mention.rdrama_comment_id - The ID of the comment from the r/Drama platform.
* @param {string} mention.username - The mentioned Reddit username.
* @param {string} [mention.message] - The content of the message sent to the mentioned user.
* @throws {Error} Will throw an error if the insert operation fails.
* const gameState = await DatabaseService.loadGameState(authorId);
* if (gameState) {
* console.log('Game state loaded successfully.');
* } else {
* console.log('No existing game state found. Initializing a new game.');
* }
*
* @param {number} authorId - The unique identifier for the author of the game.
* @returns {Promise<GameState | null>} A promise that resolves to a GameState instance if found, or null if no existing game state is present.
* @throws {Error} Will throw an error if the database operation fails.
*/
public static async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise<void> {
const db = await DatabaseService.getDatabase()
const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`;
await db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]);
}
/**
* Queries the database to check if a username has been mentioned.
*
* @example
* const mentioned = await DatabaseService.userMentionExists('exampleUser');
* console.log(mentioned ? 'User has been mentioned.' : 'User has not been mentioned.');
*
* @param {string} username - The username to search for.
* @returns {Promise<boolean>} A boolean indicating whether the username has been mentioned.
* @throws {Error} Will throw an error if the query operation fails.
*/
public static async userMentionExists(username: string): Promise<boolean> {
const db = await DatabaseService.getDatabase()
const sql = `SELECT 1 FROM user_mentions WHERE username = ?`;
const result = await db.get(sql, [username]);
return !!result;
}
/**
* Inserts or updates the OAuth token in the database for a specific service.
*
* @example
* await DatabaseService.upsertOAuthToken('https://oauth.reddit.com', {
* access_token: 'abc123',
* token_type: 'bearer',
* expires_in: 3600,
* scope: 'read'
* });
*
* @param {string} token_identifier - A unique identifier for the token, typically the service's base URL.
* @param {Object} tokenData - The OAuth token data including access_token, token_type, expires_in, and scope.
* @throws {Error} Will throw an error if the upsert operation fails.
*/
public static async upsertOAuthToken(token_identifier: string, tokenData: any) {
const db = await DatabaseService.getDatabase()
const { access_token, token_type, expires_in, scope } = tokenData;
const expiryTimestamp = Math.floor(Date.now() / 1000) + expires_in;
console.log('token_identifier', token_identifier)
console.log('access_token', `${access_token.substring(0, 5)}XXXXX`)
console.log('token_type', token_type)
console.log('expires_in', expires_in)
console.log('scope', scope)
await db.run(`
INSERT INTO oauth_tokens (token_identifier, access_token, token_type, expires_in, expiry_timestamp, scope)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(token_identifier) DO UPDATE SET
access_token = excluded.access_token,
token_type = excluded.token_type,
expires_in = excluded.expires_in,
expiry_timestamp = excluded.expiry_timestamp,
scope = excluded.scope
`, [token_identifier, access_token, token_type, expires_in, expiryTimestamp, scope]);
}
/**
* Retrieves the current, unexpired OAuth token for a specific service.
*
* @example
* const token = await DatabaseService.getCurrentOAuthToken('https://oauth.reddit.com');
* console.log(token ? `Current token: ${token.access_token}` : 'No valid token found.');
*
* @param {string} token_identifier - The unique identifier for the token, typically the service's base URL.
* @returns {Promise<Object|null>} The current OAuth token data or null if expired or not found.
* @throws {Error} Will throw an error if the query operation fails.
*/
public static async getCurrentOAuthToken(token_identifier: string) {
const db = await DatabaseService.getDatabase()
const tokenRow = await db.get(`
SELECT access_token, token_type, scope, expiry_timestamp FROM oauth_tokens
WHERE token_identifier = ?
`, token_identifier);
return tokenRow || null;
}
/**
* Checks if the cooldown period has passed since the last notification was sent, allowing for a new notification to be sent.
*
* @example
* const canSend = await DatabaseService.canSendNotification();
* console.log(canSend ? 'Can send a new notification.' : 'Still in cooldown period.');
*
* @returns {Promise<boolean>} True if the cooldown period has passed, allowing new notifications to be sent.
* @throws {Error} Will throw an error if the check operation fails.
*/
public static async canSendNotification(): Promise<boolean> {
const db = await DatabaseService.getDatabase()
const cooldownHours = process.env.NOTIFICATION_COOLDOWN_HOURS || 4;
const sql = `
SELECT MAX(sent_time) as last_notification_time
FROM user_mentions
`;
const result = await db.get(sql);
if (!result || !result.last_notification_time) {
// No notifications have been sent yet, or unable to retrieve the last sent time.
return true;
public static async loadGameState(authorId: number): Promise<GameState | null> {
const db = await DatabaseService.getDatabase();
const sql = `SELECT data FROM game_state WHERE authorId = ?`;
const row = await db.get(sql, [authorId]);
if (row) {
return JSON.parse(row.data) as GameState; // Assuming GameState constructor can take authorId and a partial state object
} else {
return null; // Or return a new GameState with defaults
}
}
const lastNotificationTime = new Date(result.last_notification_time).getTime();
const currentTime = new Date(new Date().toISOString().slice(0, 19).replace('T', ' ')).getTime();
const timeElapsed = currentTime - lastNotificationTime;
//console.log('timeElapsed', timeElapsed)
const cooldownPeriod = +cooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds
//console.log('cooldownPeriod', cooldownPeriod)
return timeElapsed >= cooldownPeriod;
/**
* Saves the current game state for a given author to the database.
* If an existing game state for the author exists, it updates the stored values. If no game state exists, it inserts a new record.
* This method ensures the game state is persisted between sessions, allowing players to resume their game at any time.
*
* @example
* await DatabaseService.saveGameState(authorId, gameState);
* console.log('Game state saved successfully.');
*
* @param {number} authorId - The unique identifier for the author of the game.
* @param {GameState} gameState - The current game state to be saved.
* @returns {Promise<void>} A promise that resolves when the game state is successfully saved.
* @throws {Error} Will throw an error if the save operation fails.
*/
public static async saveGameState(authorId: number, gameState: GameState): Promise<void> {
const db = await DatabaseService.getDatabase();
const data = JSON.stringify(gameState);
const sql = `
INSERT INTO game_state (authorId, data) VALUES (?, ?)
ON CONFLICT(authorId) DO UPDATE SET
data = excluded.data;
`;
await db.run(sql, [authorId, data]);
}
}

View File

@ -1,11 +1,11 @@
import GameState from "./gameState";
class GameEvents {
private events: OregonEvent[];
private randomEvents: OregonEvent[];
private totalThreshold: number;
constructor() {
// Initialize the events and their thresholds.
this.events = [
this.randomEvents = [
{
name: "Wagon Breakdown",
threshold: 10, // Probability weight
@ -99,14 +99,14 @@ class GameEvents {
// Add more events here...
];
this.totalThreshold = this.events.reduce((acc, event) => acc + event.threshold, 0);
this.totalThreshold = this.randomEvents.reduce((acc, event) => acc + event.threshold, 0);
}
generateEvent(gameState: GameState): string {
generateRandomEvent(gameState: GameState): string {
const randomEventNumber = Math.random() * this.totalThreshold;
let cumulativeThreshold = 0;
for (let event of this.events) {
for (let event of this.randomEvents) {
cumulativeThreshold += event.threshold;
if (randomEventNumber < cumulativeThreshold) {
console.log(`Event triggered: ${event.name}`);
@ -115,6 +115,65 @@ class GameEvents {
}
return "";
}
mountains(gameState: GameState): string {
if (gameState.totalMileageWholeTrip <= 950) {
return '';
}
let message = "**🏔️ Mountain Passage 🏔️**\n";
let encounter = false;
let randomFactor = Math.random() * 10;
let mountainDifficulty = 9 - ((gameState.totalMileageWholeTrip / 100 - 15) ** 2 + 72) / ((gameState.totalMileageWholeTrip / 100 - 15) ** 2 + 12);
if (randomFactor > mountainDifficulty) {
encounter = true;
message += "Rugged mountains make your journey difficult.\n";
if (Math.random() > 0.1) {
gameState.totalMileageWholeTrip -= 60;
message += "🔍 You got lost—losing valuable time trying to find the trail!\n";
} else if (Math.random() > 0.11) {
gameState.amountSpentOnMiscellaneousSupplies -= 5;
gameState.amountSpentOnAmmunition -= 200;
gameState.totalMileageWholeTrip -= 20 + Math.random() * 30;
message += "🛠️ Wagon damaged!—Lose time and supplies.\n";
} else {
gameState.totalMileageWholeTrip -= 45 + Math.random() / 0.02;
message += "🐢 The going gets slow.\n";
}
}
if (!gameState.clearSouthPassFlag) {
gameState.clearSouthPassFlag = true;
if (Math.random() < 0.8) {
encounter = true;
message += "✅ You made it safely through South Pass—no snow.\n";
}
}
if (gameState.totalMileageWholeTrip >= 1700 && !gameState.clearBlueMountainsFlag) {
gameState.clearBlueMountainsFlag = true;
if (Math.random() < 0.7) {
encounter = true;
gameState.blizzardFlag = true;
gameState.amountSpentOnFood -= 25;
gameState.amountSpentOnMiscellaneousSupplies -= 10;
gameState.amountSpentOnAmmunition -= 300;
gameState.totalMileageWholeTrip -= 30 + Math.random() * 40;
message += "❄️ Blizzard in mountain pass—time and supplies lost.\n";
if (gameState.amountSpentOnClothing < 18 + Math.random() * 2) {
message += "❄️🧣 Not enough clothing for the cold. This could be dire.\n";
// Add logic to affect player health or trigger a death sequence.
}
}
}
return encounter ? message : "The mountains are distant... for now.\n";
}
}
export default GameEvents;

View File

@ -14,17 +14,30 @@ class GameSession {
this.gameManager = new GameFlow(this.gameState);
}
/**
* Processes player actions and updates the game state accordingly.
* @param action The player's action.
* @param params Optional parameters for the action.
*/
public async processAction(action: string, params?: any): Promise<void> {
// Update the game state based on the action.
// This might include changing locations, spending money, hunting for food, etc.
await this.gameManager.handleAction(action, params);
// Save the current game state to allow resuming later.
this.saveGameState();
async handleUserInput(userId: string, input: string): Promise<void> {
const gameState = await this.loadGameState(userId);
switch (input.split(' ')[0].toLowerCase()) {
case "!!oregon":
await this.processCommand(gameState, input);
break;
// Additional commands as necessary
}
await this.saveGameState(gameState);
}
private async processCommand(gameState: GameState, command: string): Promise<void> {
const args = command.split(' ');
// Process different game setup commands (e.g., "start over", "buy supplies")
if (args[1].toLowerCase() === "startover") {
gameState.reset(); // Resets game state to initial values
} else if (args[1].toLowerCase() === "buysupplies") {
// Transition to handling purchases
// This would involve setting the next expected action in the game state
// And possibly sending a prompt for the first purchase
}
// Handle other commands and shopping logic
}
/**

View File

@ -1,4 +1,12 @@
import { DatabaseService } from "../db/services/Database";
/**
* Represents the state of a game session for an Oregon Trail-style game.
* This class stores all relevant game state information and provides methods
* to load from and save to a database.
*/
class GameState {
authorId: number = 0;
amountSpentOnAnimals: number = 0;
amountSpentOnAmmunition: number = 0;
actualResponseTimeForBang: number = 0;
@ -32,12 +40,48 @@ class GameState {
actionChoiceForEachTurn: number = 0;
fortOptionFlag: boolean = false;
death: boolean = false;
phase: PHASE_ENUM = PHASE_ENUM.SETUP;
subPhase: number = 0;
constructor(authorId: string) {
// Load existing session for authorId or create default values
private constructor(authorId: number, state?: Partial<GameState>) {
this.authorId = authorId;
Object.assign(this, state); // Initialize with loaded state or undefined
}
// Methods to update game state variables and check game conditions
/**
* Saves the current game state to the database.
*/
public async save() {
await DatabaseService.saveGameState(this.authorId, this);
}
/**
* Loads an existing game state from the database or creates a new one if it doesn't exist.
* @param {number} authorId - The ID of the author/player.
* @returns {Promise<GameState>} - The loaded or newly created game state.
*/
public static async load(authorId: number): Promise<GameState> {
const loadedState = await DatabaseService.loadGameState(authorId);
if (loadedState) {
return new GameState(authorId, loadedState);
} else {
// Create a new GameState with default values
const newState = new GameState(authorId);
await newState.save(); // Optionally save the new state to the database
return newState;
}
}
}
export default GameState
export default GameState
// Enumeration for different phases of the game.
enum PHASE_ENUM {
SETUP = 'SETUP',
TRAVEL = 'TRAVEL',
FORT_HUNT = 'FORT_HUNT',
ENCOUNTER = 'ENCOUNTER',
EVENT = 'EVENT',
MOUNTAIN = 'MOUNTAIN',
DEATH_CHECK = 'DEATH_CHECK',
}