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 4690 GOTO 4710
4700 REM ***MOUNTAINS*** 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 4720 IF RND(-1)*10>9-((totalMileageWholeTrip/100-15)^2+72)/((totalMileageWholeTrip/100-15)^2+12) THEN 4860
4730 PRINT "RUGGED MOUNTAINS" 4730 PRINT "RUGGED MOUNTAINS"
4740 IF RND(-1)>.1 THEN 4780 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 { Database } from 'sqlite';
import { Comment } from '../../rdrama/models/Comment';
import { DatabaseInitializer } from '../initializeDatabase'; import { DatabaseInitializer } from '../initializeDatabase';
import GameState from '../../game/gameState';
/** /**
* Service for interacting with the SQLite database for operations related to comments and user mentions. * 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. * Loads an existing game state for a given author from the database.
* This static method adds a record of a user being mentioned in a comment. * 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 * @example
* await DatabaseService.insertUserMention({ * const gameState = await DatabaseService.loadGameState(authorId);
* rdrama_comment_id: 456, * if (gameState) {
* username: 'mentionedUser', * console.log('Game state loaded successfully.');
* message: 'You were mentioned in a comment.' * } else {
* }); * console.log('No existing game state found. Initializing a new game.');
* * }
* @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 {number} authorId - The unique identifier for the author of the game.
* @param {string} mention.username - The mentioned Reddit username. * @returns {Promise<GameState | null>} A promise that resolves to a GameState instance if found, or null if no existing game state is present.
* @param {string} [mention.message] - The content of the message sent to the mentioned user. * @throws {Error} Will throw an error if the database operation fails.
* @throws {Error} Will throw an error if the insert operation fails.
*/ */
public static async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise<void> { public static async loadGameState(authorId: number): Promise<GameState | null> {
const db = await DatabaseService.getDatabase() const db = await DatabaseService.getDatabase();
const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`; const sql = `SELECT data FROM game_state WHERE authorId = ?`;
await db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]); 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 {
* Queries the database to check if a username has been mentioned. return null; // Or return a new GameState with defaults
*
* @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;
} }
}
const lastNotificationTime = new Date(result.last_notification_time).getTime(); /**
const currentTime = new Date(new Date().toISOString().slice(0, 19).replace('T', ' ')).getTime(); * Saves the current game state for a given author to the database.
const timeElapsed = currentTime - lastNotificationTime; * If an existing game state for the author exists, it updates the stored values. If no game state exists, it inserts a new record.
//console.log('timeElapsed', timeElapsed) * This method ensures the game state is persisted between sessions, allowing players to resume their game at any time.
const cooldownPeriod = +cooldownHours * 60 * 60 * 1000; // Convert hours to milliseconds *
//console.log('cooldownPeriod', cooldownPeriod) * @example
* await DatabaseService.saveGameState(authorId, gameState);
return timeElapsed >= cooldownPeriod; * 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"; import GameState from "./gameState";
class GameEvents { class GameEvents {
private events: OregonEvent[]; private randomEvents: OregonEvent[];
private totalThreshold: number; private totalThreshold: number;
constructor() { constructor() {
// Initialize the events and their thresholds. // Initialize the events and their thresholds.
this.events = [ this.randomEvents = [
{ {
name: "Wagon Breakdown", name: "Wagon Breakdown",
threshold: 10, // Probability weight threshold: 10, // Probability weight
@ -99,14 +99,14 @@ class GameEvents {
// Add more events here... // 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; const randomEventNumber = Math.random() * this.totalThreshold;
let cumulativeThreshold = 0; let cumulativeThreshold = 0;
for (let event of this.events) { for (let event of this.randomEvents) {
cumulativeThreshold += event.threshold; cumulativeThreshold += event.threshold;
if (randomEventNumber < cumulativeThreshold) { if (randomEventNumber < cumulativeThreshold) {
console.log(`Event triggered: ${event.name}`); console.log(`Event triggered: ${event.name}`);
@ -115,6 +115,65 @@ class GameEvents {
} }
return ""; 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; export default GameEvents;

View File

@ -14,17 +14,30 @@ class GameSession {
this.gameManager = new GameFlow(this.gameState); this.gameManager = new GameFlow(this.gameState);
} }
/** async handleUserInput(userId: string, input: string): Promise<void> {
* Processes player actions and updates the game state accordingly. const gameState = await this.loadGameState(userId);
* @param action The player's action.
* @param params Optional parameters for the action. switch (input.split(' ')[0].toLowerCase()) {
*/ case "!!oregon":
public async processAction(action: string, params?: any): Promise<void> { await this.processCommand(gameState, input);
// Update the game state based on the action. break;
// This might include changing locations, spending money, hunting for food, etc. // Additional commands as necessary
await this.gameManager.handleAction(action, params); }
// Save the current game state to allow resuming later.
this.saveGameState(); 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 { class GameState {
authorId: number = 0;
amountSpentOnAnimals: number = 0; amountSpentOnAnimals: number = 0;
amountSpentOnAmmunition: number = 0; amountSpentOnAmmunition: number = 0;
actualResponseTimeForBang: number = 0; actualResponseTimeForBang: number = 0;
@ -32,12 +40,48 @@ class GameState {
actionChoiceForEachTurn: number = 0; actionChoiceForEachTurn: number = 0;
fortOptionFlag: boolean = false; fortOptionFlag: boolean = false;
death: boolean = false; death: boolean = false;
phase: PHASE_ENUM = PHASE_ENUM.SETUP;
subPhase: number = 0;
constructor(authorId: string) { private constructor(authorId: number, state?: Partial<GameState>) {
// Load existing session for authorId or create default values 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',
}