Alison Kohout Board Game Logger Project

Overview

Seeking to enhance the efficiency of logging my board games, I developed a comprehensive data system that tracks gameplay and reveals performance patterns. By leveraging AI prompting techniques, I identified Flask and PostgreSQL as suitable technologies for building a functional local web application. I then successfully implemented this solution despite having no previous web development background and only recently encountering SQL. This achievement demonstrates my capacity to rapidly acquire new technical skills and tackle intricate challenges. More importantly, it marked an exciting and pivotal realization for me: with AI assistance complementing my abilities, my only limit is my imagination.

[GitHub →]

Data Entry and Management

Users can seamlessly input game details including date played, game title, notes, results, and various scores. This data is securely stored and managed using a PostgreSQL database.

Board game log screenshot
Screenshot of the board game logger. Background image credited to © Ryan Laukat / Red Raven Games.

Code Snippet: Adding a played game to the database

                     
@app.route('/add', methods=['POST'])
def add_game():
    date_played = request.form['date_played']
    game_title = request.form['game_title']
    notes = request.form.get('notes', '')
    result = request.form.get('result','')
    level = request.form.get('level','')
    my_score = request.form.get('my_score','')
    bot_score = request.form.get('bot_score','')

    conn = get_db_connection()
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO games (date_played, game_title, notes, result, level, my_score, bot_score) VALUES (%s, %s, %s, %s, %s, %s, %s)",
        (date_played, game_title, notes, result, level, my_score, bot_score)
    )
    conn.commit()

    cur.close()
    conn.close()

    # Redirect to the index page, passing stats as query parameters
    return redirect(url_for('index'))

                     
                     
    <div class="form-container">
        <h1>Log a Game</h1>
        <form action="/add" method="post">
            <label for="date_played">Date Played:</label>
            <input type="date" id="date_played" name="date_played" required>
            <br><br>
            <label for="game_title">Game Title:</label>
            <input type="text" id="game_title" name="game_title" required autocomplete="off" style="border: none;">
            <div id="add_suggestions" class="autocomplete-suggestions"></div>
            <br><br>
            <label for="result">Result:</label>
            <textarea id="result" name="result" rows="1" cols="50" placeholder="Add result here..."></textarea>
            <br><br>
            <label for="level">Level:</label>
            <textarea id="level" name="level" rows="1" cols="50" placeholder="Add my level here..."></textarea>
            <br><br>
            <label for="my_score">My Score:</label>
            <textarea id="my_score" name="my_score" rows="1" cols="50" placeholder="Add my score here..."></textarea>
            <br><br>
            <label for="bot_score">Bot Score:</label>
            <textarea id="bot_score" name="bot_score" rows="1" cols="50" placeholder="Add bot score here..."></textarea>
            <br><br>
            <label for="notes">Notes:</label>
            <textarea id="notes" name="notes" rows="4" cols="50" placeholder="Add any notes here..."></textarea>
            <br><br>
            <button type="submit">Log Game</button>
        </form>
    </div>

                     

Comprehensive Analysis

The application provides detailed statistics, highlighting top played games and game counts by week, month, and year. Users can access averages and identify the most played games over various time frames.

Board game log screenshot
Screenshot of basic board game statistics. Background image credited to © Ryan Laukat / Red Raven Games.

Code Snippet: Played board game statistics

                     
@app.route('/')
def index():
    conn = get_db_connection()
    cur = conn.cursor()

    cur.execute("SELECT DISTINCT game_title FROM games")
    game_titles = [row[0] for row in cur.fetchall()]

    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 5
    """)
    top_games = cur.fetchall()

    # Calculate date boundaries for the current week, month, and year
    today = date.today()
    start_of_week = today - timedelta(days=today.weekday())
    start_of_month = today.replace(day=1)
    start_of_year = today.replace(month=1, day=1)
    
    # Define date boundaries for last week, last month, and last year
    start_of_current_week = today - timedelta(days=today.weekday())
    end_of_last_week = start_of_current_week - timedelta(days=1)
    start_of_last_week = end_of_last_week - timedelta(days=6)

    start_of_current_month = today.replace(day=1)
    last_day_of_last_month = start_of_current_month - timedelta(days=1)
    start_of_last_month = last_day_of_last_month.replace(day=1)

    start_of_current_year = today.replace(month=1, day=1)
    last_day_of_last_year = start_of_current_year - timedelta(days=1)
    start_of_last_year = last_day_of_last_year.replace(month=1, day=1)

    # Count games played this week
    cur.execute("SELECT COUNT(*) FROM games WHERE date_played >= %s", (start_of_week,))
    games_this_week = cur.fetchone()[0]
    
    # Count games played this month
    cur.execute("SELECT COUNT(*) FROM games WHERE date_played >= %s", (start_of_month,))
    games_this_month = cur.fetchone()[0]
    
    # Count games played this year
    cur.execute("SELECT COUNT(*) FROM games WHERE date_played >= %s", (start_of_year,))
    games_this_year = cur.fetchone()[0]

    # Define date boundaries for last week, last month, and last year
    start_of_current_week = today - timedelta(days=today.weekday())
    end_of_last_week = start_of_current_week - timedelta(days=1)
    start_of_last_week = end_of_last_week - timedelta(days=6)

    start_of_current_month = today.replace(day=1)
    last_day_of_last_month = start_of_current_month - timedelta(days=1)
    start_of_last_month = last_day_of_last_month.replace(day=1)

    start_of_current_year = today.replace(month=1, day=1)
    last_day_of_last_year = start_of_current_year - timedelta(days=1)
    start_of_last_year = last_day_of_last_year.replace(month=1, day=1)

    # Games played last week
    cur.execute("SELECT COUNT(*) FROM games WHERE date_played BETWEEN %s AND %s", (start_of_last_week, end_of_last_week))
    games_last_week = cur.fetchone()[0]

    # Games played last month
    cur.execute("SELECT COUNT(*) FROM games WHERE date_played BETWEEN %s AND %s", (start_of_last_month, last_day_of_last_month))
    games_last_month = cur.fetchone()[0]

    # Games played last year
    cur.execute("SELECT COUNT(*) FROM games WHERE date_played BETWEEN %s AND %s", (start_of_last_year, last_day_of_last_year))
    games_last_year = cur.fetchone()[0]

    # Starting reference date for weekly and monthly averages calculation (removes the false 2023 data)
    start_date = date(2024, 1, 1)

    # End dates for averages (up to the start of the current period)
    end_of_last_week = start_of_week
    end_of_last_month = start_of_month
    end_of_last_year = start_of_year

    # Calculate total number of games and periods for averages
    # Weekly Average Calculation
    total_days = (end_of_last_week - start_date).days
    num_weeks = total_days // 7 if total_days >= 7 else 0

    if num_weeks > 0:
        cur.execute("SELECT COUNT(*) FROM games WHERE date_played >= %s AND date_played < %s", (start_date, end_of_last_week))
        total_games = cur.fetchone()[0]
        weekly_avg = round(total_games / num_weeks)
    else:
        weekly_avg = 0

    # Monthly Average Calculation
    num_months = (end_of_last_month.year - start_date.year) * 12 + (end_of_last_month.month - start_date.month)
    if num_months > 0:
        cur.execute("SELECT COUNT(*) FROM games WHERE date_played >= %s AND date_played < %s", (start_date, end_of_last_month))
        total_games = cur.fetchone()[0]
        monthly_avg = round(total_games / num_months)
    else:
        monthly_avg = 0

    # Yearly Average Calculation
    start_date = date(2023, 1, 1)
    num_years = end_of_last_year.year - start_date.year
    if num_years > 0:
        cur.execute("SELECT COUNT(*) FROM games WHERE date_played >= %s AND date_played < %s", (start_date, end_of_last_year))
        total_games = cur.fetchone()[0]
        yearly_avg = round(total_games / num_years)
    else:
        yearly_avg = 0

    # Most played game this week
    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        WHERE date_played >= %s
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 1
    """, (start_of_week,))
    most_played_this_week = cur.fetchone()
    if most_played_this_week:
        most_played_game_week = most_played_this_week[0]
        week_play_count = most_played_this_week[1]
    else:
        most_played_game_week = None
        week_play_count = 0

    # Most played game this month
    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        WHERE date_played >= %s
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 1
    """, (start_of_month,))
    most_played_this_month = cur.fetchone()
    if most_played_this_month:
        most_played_game_month = most_played_this_month[0]
        month_play_count = most_played_this_month[1]
    else:
        most_played_game_month = None
        month_play_count = 0

    # Most played game this year
    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        WHERE date_played >= %s
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 1
    """, (start_of_year,))
    most_played_this_year = cur.fetchone()
    if most_played_this_year:
        most_played_game_year = most_played_this_year[0]
        year_play_count = most_played_this_year[1]
    else:
        most_played_game_year = None
        year_play_count = 0

   # Most played game last week
    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        WHERE date_played BETWEEN %s AND %s
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 1
    """, (start_of_last_week, end_of_last_week))
    most_played_last_week = cur.fetchone()
    if most_played_last_week:
        most_played_game_last_week = most_played_last_week[0]
        last_week_play_count = most_played_last_week[1]
    else:
        most_played_game_last_week = None
        last_week_play_count = 0

    # Most played game last month
    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        WHERE date_played BETWEEN %s AND %s
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 1
    """, (start_of_last_month, last_day_of_last_month))
    most_played_last_month = cur.fetchone()
    if most_played_last_month:
        most_played_game_last_month = most_played_last_month[0]
        last_month_play_count = most_played_last_month[1]
    else:
        most_played_game_last_month = None
        last_month_play_count = 0

    # Most played game last year
    cur.execute("""
        SELECT game_title, COUNT(*) as play_count
        FROM games
        WHERE date_played BETWEEN %s AND %s
        GROUP BY game_title
        ORDER BY play_count DESC
        LIMIT 1
    """, (start_of_last_year, last_day_of_last_year))
    most_played_last_year = cur.fetchone()
    if most_played_last_year:
        most_played_game_last_year = most_played_last_year[0]
        last_year_play_count = most_played_last_year[1]
    else:
        most_played_game_last_year = None
        last_year_play_count = 0

    cur.close()
    conn.close()
    return render_template(
        'index.html',
        game_titles=sorted(game_titles),
        top_games=top_games,
        today=date.today().isoformat(),
        games_this_week=games_this_week,
        games_this_month=games_this_month,
        games_this_year=games_this_year,
        games_last_week=games_last_week,
        games_last_month=games_last_month,
        games_last_year=games_last_year,
        most_played_game_week=most_played_game_week,
        most_played_game_month=most_played_game_month,
        most_played_game_year=most_played_game_year,
        week_play_count=week_play_count,
        month_play_count=month_play_count,
        year_play_count=year_play_count,
    most_played_game_last_week=most_played_game_last_week,
    most_played_game_last_month=most_played_game_last_month,
    most_played_game_last_year=most_played_game_last_year,
    last_week_play_count=last_week_play_count,
    last_month_play_count=last_month_play_count,
    last_year_play_count=last_year_play_count,
        weekly_avg = weekly_avg,
        monthly_avg = monthly_avg,
        yearly_avg = yearly_avg
    )


                     
                     
<div class="form-container">
    <h2>Games played</h2>
    <table style="border-collapse: separate; border-spacing: 20px 0;">
      <tr>
        <th style="padding: 10px; text-align: left;"></th>
        <th style="padding: 10px; text-align: left;">Week</th>
        <th style="padding: 10px; text-align: left;">Month</th>
        <th style="padding: 10px; text-align: left;">Year</th>
      </tr>
      <tr>
        <td style="padding: 10px;">Current</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Previous</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Average</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
    </table>

    <h2>Most Played Games</h2>
    <table style="border-collapse: separate; border-spacing: 20px 0;">
      <tr>
        <th style="padding: 10px; text-align: left;"></th>
        <th style="padding: 10px; text-align: left;">Game</th>
        <th style="padding: 10px; text-align: left;">Plays</th>
      </tr>
      <tr>
        <td style="padding: 10px;">Current Week</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Previous Week</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Current Month</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Previous Month</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Current Year</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
      <tr>
        <td style="padding: 10px;">Previous Year</td>
        <td style="padding: 10px;"></td>
        <td style="padding: 10px;"></td>
      </tr>
    </table>

</div>

                     

Special Features for Enthusiasts

The project includes targeted features for Sleeping Gods, including totem tracking and resource management, as well as detailed win/loss statistics for the Imperium game—appealing to dedicated gamers.

Board game log screenshot
A screenshot of all the logged details for the board game Sleeping Gods. Sleeping Gods is an adventure game about exploring a world. Keeping notes as you play is an essential aspect of the game. Background image credited to © Ryan Laukat / Red Raven Games.
Board game log screenshot
A screenshot of the search options specifically developed for the board game Sleeping Gods. This is an essential tool for the adventure board game to help you make decisions about where to go and what to do. Background image credited to © Ryan Laukat / Red Raven Games.
Code Snippet: Tracking games in Sleeping Gods
                     
@app.route('/sleeping_gods', methods=['GET', 'POST'])
def sleeping_gods():

    if request.method == 'POST':

        # Get other form data
        location_raw = request.form.get('location', '').strip()
        location = int(location_raw) if location_raw else 0
        part = request.form.get('part', '')
        required_keyword = request.form.get('required_keyword', '')
        gained_keyword = request.form.get('gained_keyword', '')
        visited = '1' if request.form.get('visited') == '1' else '0'
        notes = request.form.get('notes', '')
        combat = '1' if request.form.get('combat') == '1' else '0'
        combat_level_raw = request.form.get('combat_level', '').strip()
        combat_level = int(combat_level_raw) if combat_level_raw else 0
        gained = request.form.get('gained', '')
        req_coins_raw = request.form.get('req_coins', '').strip()
        req_coins = int(req_coins_raw) if req_coins_raw else 0
        req_meat_raw = request.form.get('req_meat', '').strip()
        req_meat = int(req_meat_raw) if req_meat_raw else 0
        req_veg_raw = request.form.get('req_veg', '').strip()
        req_veg = int(req_veg_raw) if req_veg_raw else 0
        req_grain_raw = request.form.get('req_grain', '').strip()
        req_grain = int(req_grain_raw) if req_grain_raw else 0
        req_wood_raw = request.form.get('req_wood', '').strip()
        req_wood = int(req_wood_raw) if req_wood_raw else 0
        req_artifacts_raw = request.form.get('req_artifacts', '').strip()
        req_artifacts = int(req_artifacts_raw) if req_artifacts_raw else 0
        gain_coins_raw = request.form.get('gain_coins', '').strip()
        gain_coins = int(gain_coins_raw) if gain_coins_raw else 0
        gain_meat_raw = request.form.get('gain_meat', '').strip()
        gain_meat = int(gain_meat_raw) if gain_meat_raw else 0
        gain_veg_raw = request.form.get('gain_veg', '').strip()
        gain_veg = int(gain_veg_raw) if gain_veg_raw else 0
        gain_grain_raw = request.form.get('gain_grain', '').strip()
        gain_grain = int(gain_grain_raw) if gain_grain_raw else 0
        gain_wood_raw = request.form.get('gain_wood', '').strip()
        gain_wood = int(gain_wood_raw) if gain_wood_raw else 0
        gain_artifacts_raw = request.form.get('gain_artifacts', '').strip()
        gain_artifacts = int(gain_artifacts_raw) if gain_artifacts_raw else 0
        gain_xp_raw = request.form.get('gain_xp', '').strip()
        gain_xp = int(gain_xp_raw) if gain_xp_raw else 0
        gain_adventure_raw = request.form.get('gain_adventure', '').strip()
        gain_adventure = int(gain_adventure_raw) if gain_adventure_raw else 0
        gain_ship_damage_raw = request.form.get('gain_ship_damage', '').strip()
        gain_ship_damage = int(gain_ship_damage_raw) if gain_ship_damage_raw else 0
        gain_ship_repair_raw = request.form.get('gain_ship_repair', '').strip()
        gain_ship_repair = int(gain_ship_repair_raw) if gain_ship_repair_raw else 0
        gain_crew_damage_raw = request.form.get('gain_crew_damage', '').strip()
        gain_crew_damage = int(gain_crew_damage_raw) if gain_crew_damage_raw else 0
        gain_crew_health_raw = request.form.get('gain_crew_health', '').strip()
        gain_crew_health = int(gain_crew_health_raw) if gain_crew_health_raw else 0
        gain_low_morale_raw = request.form.get('gain_low_morale', '').strip()
        gain_low_morale = int(gain_low_morale_raw) if gain_low_morale_raw else 0
        gain_fright_raw = request.form.get('gain_fright', '').strip()
        gain_fright = int(gain_fright_raw) if gain_fright_raw else 0
        gain_venom_raw = request.form.get('gain_venom', '').strip()
        gain_venom = int(gain_venom_raw) if gain_venom_raw else 0
        gain_weakness_raw = request.form.get('gain_weakness', '').strip()
        gain_weakness = int(gain_weakness_raw) if gain_weakness_raw else 0
        gain_madness_raw = request.form.get('gain_madness', '').strip()
        gain_madness = int(gain_madness_raw) if gain_madness_raw else 0
        remove_low_morale_raw = request.form.get('remove_low_morale', '').strip()
        remove_low_morale = int(remove_low_morale_raw) if remove_low_morale_raw else 0
        remove_fright_raw = request.form.get('remove_fright', '').strip()
        remove_fright = int(remove_fright_raw) if remove_fright_raw else 0
        remove_venom_raw = request.form.get('remove_venom', '').strip()
        remove_venom = int(remove_venom_raw) if remove_venom_raw else 0
        remove_weakness_raw = request.form.get('remove_weakness', '').strip()
        remove_weakness = int(remove_weakness_raw) if remove_weakness_raw else 0
        remove_madness_raw = request.form.get('remove_madness', '').strip()
        remove_madness = int(remove_madness_raw) if remove_madness_raw else 0
        gain_totem = request.form.get('gain_totem', '')
        challenge = request.form.get('challenge', '')
        challenge_level_raw = request.form.get('challenge_level', '').strip()
        challenge_level = int(challenge_level_raw) if challenge_level_raw else 0

        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute(
            "INSERT INTO sleeping_gods (location, part, required_keyword, gained_keyword, visited, notes, combat, combat_level, gained, req_coins, req_meat, req_veg, req_grain, req_wood, req_artifacts, gain_coins, gain_meat, gain_veg, gain_grain, gain_wood, gain_artifacts, gain_xp, gain_ship_damage, gain_ship_repair, gain_crew_damage, gain_crew_health, gain_low_morale, gain_fright, gain_venom, gain_weakness, gain_madness, remove_low_morale, remove_fright, remove_venom, remove_weakness, remove_madness, gain_totem, challenge, challenge_level, gain_adventure) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
            (location, part, required_keyword, gained_keyword, visited, notes, combat, combat_level, gained, req_coins, req_meat, req_veg, req_grain, req_wood, req_artifacts, gain_coins, gain_meat, gain_veg, gain_grain, gain_wood, gain_artifacts, gain_xp, gain_ship_damage, gain_ship_repair, gain_crew_damage, gain_crew_health, gain_low_morale, gain_fright, gain_venom, gain_weakness, gain_madness, remove_low_morale, remove_fright, remove_venom, remove_weakness, remove_madness, gain_totem, challenge, challenge_level, gain_adventure)
        )
        conn.commit()

        cur.close()
        conn.close()

    return render_template(
        'sleeping_gods.html',
    )

@app.route('/search_sleeping_gods_location', methods=['GET'])
def search_sleeping_gods_location():
    location = request.args.get('term', '0')
    conn = get_db_connection()
    cur = conn.cursor()

    # Search  
    cur.execute("""
        SELECT id,* FROM sleeping_gods  
        WHERE location = %s 
    """, (location,))
    results = cur.fetchall()
    cur.close()
    conn.close()

    # Convert the results to a list of dictionaries
    data = [
        {       
	    'id': row[0],
	    'location': row[1],
	    'part': row[2],
	    'required_keyword': row[3],
	    'gained_keyword': row[4],
	    'visited': row[5],
	    'notes': row[6],
	    'combat': row[7],
	    'combat_level': row[8],
	    'gained': row[9],
	    'lost': row[10],
	    'req_coins': row[11],
	    'req_meat': row[12],
	    'req_veg': row[13],
	    'req_grain': row[14],
	    'req_artifacts': row[15],
	    'gain_coins': row[16],
	    'gain_meat': row[17],
	    'gain_veg': row[18],
	    'gain_grain': row[19],
	    'gain_artifacts': row[20],
	    'req_wood': row[21],
	    'gain_wood': row[22],
	    'gain_xp': row[23],
	    'gain_ship_damage': row[24],
	    'gain_ship_repair': row[25],
	    'gain_crew_damage': row[26],
	    'gain_crew_health': row[27],
	    'gain_low_morale': row[28],
	    'gain_fright': row[29],
	    'gain_venom': row[30],
	    'gain_weakness': row[31],
	    'gain_madness': row[32],
	    'remove_low_morale': row[33],
	    'remove_fright': row[34],
	    'remove_venom': row[35],
	    'remove_weakness': row[36],
	    'remove_madness': row[37],
	    'totem': row[38],
	    'challenge': row[39],
	    'challenge_level': row[40],
	    'gain_totem': row[41],
	    'gain_adventure': row[42],
        }
        for row in results
    ]

    return jsonify(data)

@app.route('/search_sleeping_gods_notes', methods=['GET'])
def search_sleeping_gods_notes():
    keyword = request.args.get('term', '0')
    conn = get_db_connection()
    cur = conn.cursor()

    # Search  
    cur.execute("""
        SELECT id,* FROM sleeping_gods  
        WHERE notes ILIKE %s 
           OR required_keyword ILIKE %s
           OR gained_keyword ILIKE %s
	   OR gained ILIKE %s
	   OR lost ILIKE %s
    """, (f"%{keyword}%", f"%{keyword}%",f"%{keyword}%",f"%{keyword}%",f"%{keyword}%"))
    results = cur.fetchall()
    cur.close()
    conn.close()

    # Convert the results to a list of dictionaries
    data = [
        {       
	    'id': row[0],
	    'location': row[1],
	    'part': row[2],
	    'required_keyword': row[3],
	    'gained_keyword': row[4],
	    'visited': row[5],
	    'notes': row[6],
	    'combat': row[7],
	    'combat_level': row[8],
	    'gained': row[9],
	    'lost': row[10],
	    'req_coins': row[11],
	    'req_meat': row[12],
	    'req_veg': row[13],
	    'req_grain': row[14],
	    'req_artifacts': row[15],
	    'gain_coins': row[16],
	    'gain_meat': row[17],
	    'gain_veg': row[18],
	    'gain_grain': row[19],
	    'gain_artifacts': row[20],
	    'req_wood': row[21],
	    'gain_wood': row[22],
	    'gain_xp': row[23],
	    'gain_ship_damage': row[24],
	    'gain_ship_repair': row[25],
	    'gain_crew_damage': row[26],
	    'gain_crew_health': row[27],
	    'gain_low_morale': row[28],
	    'gain_fright': row[29],
	    'gain_venom': row[30],
	    'gain_weakness': row[31],
	    'gain_madness': row[32],
	    'remove_low_morale': row[33],
	    'remove_fright': row[34],
	    'remove_venom': row[35],
	    'remove_weakness': row[36],
	    'remove_madness': row[37],
	    'totem': row[38],
	    'challenge': row[39],
	    'challenge_level': row[40],
	    'gain_totem': row[41],
	    'gain_adventure': row[42],
        }
        for row in results
    ]

    return jsonify(data)

@app.route('/search_sleeping_gods_keyword', methods=['GET'])
def search_sleeping_gods_keyword():
    keyword = request.args.get('term', '')
    conn = get_db_connection()
    cur = conn.cursor()

    # Search  
    cur.execute("""
        SELECT id,* FROM sleeping_gods  
        WHERE required_keyword ILIKE %s OR
              gained_keyword ILIKE %s 
    """, (f"%{keyword}%", f"%{keyword}%"))
    results = cur.fetchall()
    cur.close()
    conn.close()

    # Convert the results to a list of dictionaries
    data = [
        {       
	    'id': row[0],
	    'location': row[1],
	    'part': row[2],
	    'required_keyword': row[3],
	    'gained_keyword': row[4],
	    'visited': row[5],
	    'notes': row[6],
	    'combat': row[7],
	    'combat_level': row[8],
	    'gained': row[9],
	    'lost': row[10],
	    'req_coins': row[11],
	    'req_meat': row[12],
	    'req_veg': row[13],
	    'req_grain': row[14],
	    'req_artifacts': row[15],
	    'gain_coins': row[16],
	    'gain_meat': row[17],
	    'gain_veg': row[18],
	    'gain_grain': row[19],
	    'gain_artifacts': row[20],
	    'req_wood': row[21],
	    'gain_wood': row[22],
	    'gain_xp': row[23],
	    'gain_ship_damage': row[24],
	    'gain_ship_repair': row[25],
	    'gain_crew_damage': row[26],
	    'gain_crew_health': row[27],
	    'gain_low_morale': row[28],
	    'gain_fright': row[29],
	    'gain_venom': row[30],
	    'gain_weakness': row[31],
	    'gain_madness': row[32],
	    'remove_low_morale': row[33],
	    'remove_fright': row[34],
	    'remove_venom': row[35],
	    'remove_weakness': row[36],
	    'remove_madness': row[37],
	    'totem': row[38],
	    'challenge': row[39],
	    'challenge_level': row[40],
	    'gain_totem': row[41],
	    'gain_adventure': row[42],
        }
        for row in results
    ]

    return jsonify(data)

@app.route('/delete_sleeping_gods_row', methods=['POST'])
def delete_sleeping_gods_row():
    data = request.get_json(force=True)
    row_id = data.get('id')
    if not row_id:
        return jsonify({'success': False, 'error': 'id missing'}), 400

    try:
        with get_db_connection() as conn, conn.cursor() as cur:
            cur.execute("DELETE FROM sleeping_gods WHERE id = %s", (row_id,))
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/reset_visited_sleeping_gods', methods=['POST'])
def reset_visited_sleeping_gods():
    try:
        # Connect to your database
        conn = get_db_connection()
        cursor = conn.cursor()
        
        # Update all records to set visited to 0
        cursor.execute('UPDATE sleeping_gods SET visited = FALSE')
        
        # Commit the changes
        conn.commit()
        
        # Close the connection
        cursor.close()
        conn.close()
        
        return jsonify({'success': True})

    except Exception as e:
        print(e)  # For debugging
        return jsonify({'success': False, 'error': str(e)})

@app.route('/sleeping_gods_totems_update', methods=['POST'])
def sleeping_gods_totems_update():
    try:
        data = request.get_json()
        totem_id = data['totemId']
        is_found = data['isFound']

        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute("UPDATE sleeping_gods_totems SET found = %s WHERE id = %s", (bool(is_found), totem_id))
        conn.commit()
        cur.close()
        conn.close()
        return jsonify({'success': True})
    except Exception as e:
        print(f"Error updating totem checklist: {e}")
        return jsonify({'success': False, 'message': str(e)}), 500  #Return error message

@app.route('/sleeping_gods_totems')
def sleeping_gods_totems():
    conn = get_db_connection()
    cur = conn.cursor()
    cur.execute("SELECT id, totem, found FROM sleeping_gods_totems ORDER BY id") 
    rows = cur.fetchall()
    cur.close()
    conn.close()

    totems = [{'id': row[0], 'totem': row[1], 'found': row[2]} for row in rows]
    return render_template('sleeping_gods_totems.html', totems=totems)

                     
                     
    <h1></h1>
    <button onclick="window.location.href='/'">Back to Home</button>

    <div class="form-container">
        <h1>Location Log</h1>
        <form action="/sleeping_gods" method="post">
            <label for="location">Location:</label>
            <input type="number" id="location" name="location" >
            <label for="part">&emsp;Part:</label>
            <input type="text" id="part" name="part" rows="1" cols="10" placeholder="Add part i.e. ABC here...">
            <br><br>
            <label for="visited">Visited:</label>
	    <input type="checkbox" id="visited" name="visited" value="1">
	    <button type="button" id="reset_visited" class="reset-button">Reset</button>

	    <h2>Required </h2>
            <label for="required_keyword">Keyword:</label>
            <input type="text" id="required_keyword" name="required_keyword" placeholder="Add required keyword here...">
            <br><br>
	    <table class="no-lines">
	    <tr>
	      <td>Combat Level</td>
              <td><input type="number" id="combat_level" name="combat_level"></td>
              <td>Challenge</td>
                <td>
                    <select id="challenge" name="challenge">
                        <option value="">Select Challenge</option>
                        <option value="CUNNING">CUNNING</option>
                        <option value="CRAFT">CRAFT</option>
                        <option value="PERCEPTION">PERCEPTION</option>
                        <option value="STRENGTH">STRENGTH</option>
                        <option value="SAVVY">SAVVY</option>
                    </select>
                </td>
              <td>Challenge Level</td>
              <td><input type="number" id="challenge_level" name="challenge_level" ></td>
	    </tr>
	    <tr style="height: 20px;">
	        <td colspan="2"></td> <!-- Empty row with height for spacing -->
	    </tr>
	    <tr>
	      <td>Coins</td>
              <td><input type="number" id="req_coins" name="req_coins" ></td>
	      <td>Meat</td>
              <td><input type="number" id="req_meat" name="req_meat" ></td>
	      <td>Veg</td>
              <td><input type="number" id="req_veg" name="req_veg" ></td>
	    </tr>
	    <tr>
	      <td>Grain</td>
              <td><input type="number" id="req_grain" name="req_grain" ></td>
	      <td>Materials</td>
              <td><input type="number" id="req_wood" name="req_wood" ></td>
	      <td>Artifacts</td>
              <td><input type="number" id="req_artifacts" name="req_artifacts" ></td>
	    </tr>
	    </table>

	    <h2>Gained </h2>
            <label for="gained_keyword">Keyword:</label>
            <input type="text" id="gained_keyword" name="gained_keyword" placeholder="Add gained keyword here...">
            <br><br>
	    <table class="no-lines">		
	    <tr>
	      <td>XP</td>
              <td><input type="number" id="gain_xp" name="gain_xp" ></td>
	      <td>Totem</td>
              <td><input type="text" id="gain_totem" name="gain_totem" ></td>
	      <td>Adventure</td>
              <td><input type="number" id="gain_adventure" name="gain_adventure" ></td>
	    </tr>
	    <tr style="height: 20px;">
	        <td colspan="2"></td> <!-- Empty row with height for spacing -->
	    </tr>
	    <tr>
	      <td>Coins</td>
              <td><input type="number" id="gain_coins" name="gain_coins" ></td>
	      <td>Meat</td>
              <td><input type="number" id="gain_meat" name="gain_meat" ></td>
	      <td>Veg</td>
              <td><input type="number" id="gain_veg" name="gain_veg" ></td>
	    </tr>
	    <tr>
	      <td>Grain</td>
              <td><input type="number" id="gain_grain" name="gain_grain" ></td>
	      <td>Materials</td>
              <td><input type="number" id="gain_wood" name="gain_wood" ></td>
	      <td>Artifacts</td>
              <td><input type="number" id="gain_artifacts" name="gain_artifacts" ></td>
	    </tr>
	    <tr style="height: 20px;">
	        <td colspan="2"></td> <!-- Empty row with height for spacing -->
	    </tr>
	    <tr>
	      <td>Ship Damage</td>
              <td><input type="number" id="gain_ship_damage" name="gain_ship_damage" ></td>
	      <td>Ship Repair</td>
              <td><input type="number" id="gain_ship_repair" name="gain_ship_repair" ></td>
	    </tr>
	    <tr>
	      <td>Crew Damage</td>
              <td><input type="number" id="gain_crew_damage" name="gain_crew_damage" ></td>
	      <td>Crew Health</td>
              <td><input type="number" id="gain_crew_health" name="gain_crew_health" ></td>
	    </tr>
	    <tr style="height: 20px;">
	        <td colspan="2"></td> <!-- Empty row with height for spacing -->
	    </tr>
	    <tr>
	      <td>Low Morale</td>
              <td><input type="number" id="gain_low_morale" name="gain_low_morale" ></td>
	      <td>Frightened</td>
              <td><input type="number" id="gain_fright" name="gain_fright" ></td>
	      <td>Venom</td>
              <td><input type="number" id="gain_venom" name="gain_venom" ></td>
	    </tr>
	    <tr>
	      <td>Weakened</td>
              <td><input type="number" id="gain_weakness" name="gain_weakness" ></td>
	      <td>Madness</td>
              <td><input type="number" id="gain_madness" name="gain_madness" ></td>
	    </tr>
	    </table>		
	    <h2>Removed </h2>
	    <table class="no-lines">		
	    <tr>
	      <td>Low Morale</td>
              <td><input type="number" id="remove_low_morale" name="remove_low_morale" ></td>
	      <td>Frightened</td>
              <td><input type="number" id="remove_fright" name="remove_fright" ></td>
	      <td>Venom</td>
              <td><input type="number" id="remove_venom" name="remove_venom" ></td>
	    </tr>
	    <tr>
	      <td>Weakened</td>
              <td><input type="number" id="remove_weakness" name="remove_weakness" ></td>
	      <td>Madness</td>
              <td><input type="number" id="remove_madness" name="remove_madness" ></td>
	    </tr>
	    </table>		

            <br><br>
            <label for="notes">Notes:</label>
            <textarea id="notes" name="notes" placeholder="Add any notes here..."></textarea>
            <br><br>
	    <div class="loc" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 30px;">
	        <div> <button type="submit">Log</button> </div>
	        <div class="defeat-counter-inline">
	            <span id="defeatCount"> 0 </span>
	            <button class="counter-button" onclick="incrementCounter()">Defeat</button>
	            <button class="reset-button" onclick="resetCounter()">Reset</button>
	        </div>
	    </div>
        </form>
    </div>

    <div class="form-container">
        <h2>Search Location</h2>
        <label for="search_location">Location:</label>
        <input type="number" id="search_location" name="search_location" placeholder="Enter location" >
        <button id="search_button">Search</button>
        <p id="search_result"></p>
    </div>

    <div class="form-container">
        <h2>Search Keyword</h2>
        <label for="search_keyword">Keyword:</label>
        <input type="text" id="search_keyword" name="search_keyword" placeholder="Enter keyword" >
        <button id="search_button_keyword">Search</button>
        <p id="search_result_keyword"></p>
    </div>

    <div class="form-container">
        <h2>Search Notes</h2>
        <label for="search_notes">Notes:</label>
        <input type="text" id="search_notes" name="search_notes" placeholder="Enter a word or phrase to search" >
        <button id="search_button_notes">Search</button>
        <p id="search_result_notes"></p>
    </div>

    <div class="form-container">
        <h2>Token Checklist</h2>
        <button onclick="window.location.href='/sleeping_gods_totems'" id="token_checklist">Enter</button>
    </div>

    <script>
        $(document).ready(function() {

        // Add reset visited functionality
        $('#reset_visited').click(function() {
            if (confirm('Are you sure you want to reset all visited locations to "No"?')) {
                $.ajax({
                    url: '/reset_visited_sleeping_gods',
                    type: 'POST',
                    success: function(response) {
                        if (response.success) {
                            alert('Successfully reset all visited locations');
                            // Refresh the page or update the display
                            location.reload();
                        } else {
                            alert('Error resetting visited locations');
                        }
                    },
                    error: function() {
                        alert('Error connecting to server');
                    }
                });
            }
        });
 
	// Search Location
	$('#search_button').click(function() {
	    let searchLocation = $('#search_location').val();
	    $.get('/search_sleeping_gods_location', { term: searchLocation }, function(data) {
	        if (data.length > 0) {
                    let tableHtml = '<table id="searchOutput">';   
            
	            // Header row with locations
	                tableHtml += '<tr><th></th>'; // Empty corner cell
	                data.forEach(entry => {
	                    tableHtml += `
                                <th style="text-align:center;">
					Location ${entry.location}<br>${entry.part || ''}
					<br>
					<button class="deleteRow" data-id="${entry.id}">✖</button>
				</th> `;
	                });
	                tableHtml += '</tr>';
	    
	                // Fixed rows for each attribute
	                const rows = [
 			    { label: '<h2>Required</h2>'},
	                    { label: 'Keyword', field: 'required_keyword' },
	                    { label: 'Visited', field: 'visited' },
	                    { label: 'Combat Level', field: 'combat_level' },
	                    { label: 'Challenge', field: 'challenge' },
	                    { label: 'Challenge Level', field: 'challenge_level' },
	                    { label: 'Coins', field: 'req_coins' },
	                    { label: 'Meat', field: 'req_meat' },
	                    { label: 'Veg', field: 'req_veg' },
	                    { label: 'Grain', field: 'req_grain' },
	                    { label: 'Materials', field: 'req_wood' },
	                    { label: 'Artifacts', field: 'req_artifacts' },
 			    { label: '<h2>Gained</h2>'},
	                    { label: 'Keyword', field: 'gained_keyword' },
	                    { label: 'Coins', field: 'gain_coins' },
	                    { label: 'Meat', field: 'gain_meat' },
	                    { label: 'Veg', field: 'gain_veg' },
	                    { label: 'Grain', field: 'gain_grain' },
	                    { label: 'Materials', field: 'gain_wood' },
	                    { label: 'Artifacts', field: 'gain_artifacts' },
	                    { label: 'XP', field: 'gain_xp' },
	                    { label: 'Totem', field: 'gain_totem' },
	                    { label: 'Adventure', field: 'gain_adventure' },
	                    { label: 'Ship Damage', field: 'gain_ship_damage' },
	                    { label: 'Ship Repair', field: 'gain_ship_repair' },
	                    { label: 'Crew Damage', field: 'gain_crew_damage' },
	                    { label: 'Crew Health', field: 'gain_crew_health' },
	                    { label: 'Low Morale', field: 'gain_low_morale' },
	                    { label: 'Frightened', field: 'gain_fright' },
	                    { label: 'Venom', field: 'gain_venom' },
	                    { label: 'Weakened', field: 'gain_weakness' },
	                    { label: 'Madness', field: 'gain_madness' },
 			    { label: '<h2>Removed</h2>'},
	                    { label: 'Low Morale', field: 'remove_low_morale' },
	                    { label: 'Frightened', field: 'remove_fright' },
	                    { label: 'Venom', field: 'remove_venom' },
	                    { label: 'Weakened', field: 'remove_weakness' },
	                    { label: 'Madness', field: 'remove_madness' },
			    { label: '<h2>Notes</h2>' }, 
			    { label: 'Notes', field: 'notes' }   
	                ];

			// Create each row
			rows.forEach(row => {
			    // Skip header rows (containing h2 tags)
			    if (row.label.includes('<h2>')) {
			        tableHtml += `<tr><td>${row.label}</td>`;
			        data.forEach(() => {
			            tableHtml += '<td></td>';
			        });
			        tableHtml += '</tr>';
			        return;
			    }

			    // Only create row if it has non-empty values
			    if (row.field && hasNonEmptyValues(data, row.field)) {
			        tableHtml += `<tr><td>${row.label}</td>`;
			        data.forEach(entry => {
			            let value = entry[row.field];
			            // Special handling for boolean values
			            if (row.field === 'visited' || row.field === 'combat') {
			                value = value ? 'Yes' : 'No';
			            }
			            // Don't show 0 values for numeric fields
			            if (typeof value === 'number' && value === 0) {
			                value = '';
			            }
			            tableHtml += `<td style="text-align: center;">${value || ''}</td>`;
			        });
			        tableHtml += '</tr>';
			    }
			});

	                tableHtml += '</table>';
	                $('#search_result').html(tableHtml);
	            } else {
	                $('#search_result').html('<p>No results found.</p>');
	            }
	        });
	    });

        // Search keyword
        $('#search_button_keyword').click(function() {
            let searchKeyword = $('#search_keyword').val();
            $.get('/search_sleeping_gods_keyword', { term: searchKeyword }, function(data) {
	        if (data.length > 0) {
                    let tableHtml = '<table id="searchOutput">';   
            
	            // Header row with locations
	                tableHtml += '<tr><th></th>'; // Empty corner cell
	                data.forEach(entry => {
	                    tableHtml += `
                                <th style="text-align:center;">
					Location ${entry.location}<br>${entry.part || ''}
					<br>
					<button class="deleteRow" data-id="${entry.id}">✖</button>
				</th> `;
	                });
	                tableHtml += '</tr>';
	    
	                // Fixed rows for each attribute
	                const rows = [
 			    { label: '<h2>Required</h2>'},
	                    { label: 'Keyword', field: 'required_keyword' },
	                    { label: 'Visited', field: 'visited' },
	                    { label: 'Combat Level', field: 'combat_level' },
	                    { label: 'Challenge', field: 'challenge' },
	                    { label: 'Challenge Level', field: 'challenge_level' },
	                    { label: 'Coins', field: 'req_coins' },
	                    { label: 'Meat', field: 'req_meat' },
	                    { label: 'Veg', field: 'req_veg' },
	                    { label: 'Grain', field: 'req_grain' },
	                    { label: 'Materials', field: 'req_wood' },
	                    { label: 'Artifacts', field: 'req_artifacts' },
 			    { label: '<h2>Gained</h2>'},
	                    { label: 'Keyword', field: 'gained_keyword' },
	                    { label: 'Coins', field: 'gain_coins' },
	                    { label: 'Meat', field: 'gain_meat' },
	                    { label: 'Veg', field: 'gain_veg' },
	                    { label: 'Grain', field: 'gain_grain' },
	                    { label: 'Materials', field: 'gain_wood' },
	                    { label: 'Artifacts', field: 'gain_artifacts' },
	                    { label: 'XP', field: 'gain_xp' },
	                    { label: 'Totem', field: 'gain_totem' },
	                    { label: 'Adventure', field: 'gain_adventure' },
	                    { label: 'Ship Damage', field: 'gain_ship_damage' },
	                    { label: 'Ship Repair', field: 'gain_ship_repair' },
	                    { label: 'Crew Damage', field: 'gain_crew_damage' },
	                    { label: 'Crew Health', field: 'gain_crew_health' },
	                    { label: 'Low Morale', field: 'gain_low_morale' },
	                    { label: 'Frightened', field: 'gain_fright' },
	                    { label: 'Venom', field: 'gain_venom' },
	                    { label: 'Weakened', field: 'gain_weakness' },
	                    { label: 'Madness', field: 'gain_madness' },
 			    { label: '<h2>Removed</h2>'},
	                    { label: 'Low Morale', field: 'remove_low_morale' },
	                    { label: 'Frightened', field: 'remove_fright' },
	                    { label: 'Venom', field: 'remove_venom' },
	                    { label: 'Weakened', field: 'remove_weakness' },
	                    { label: 'Madness', field: 'remove_madness' },
			    { label: '<h2>Notes</h2>' }, 
			    { label: 'Notes', field: 'notes' }   
	                ];

			// Create each row
			rows.forEach(row => {
			    // Skip header rows (containing h2 tags)
			    if (row.label.includes('<h2>')) {
			        tableHtml += `<tr><td>${row.label}</td>`;
			        data.forEach(() => {
			            tableHtml += '<td></td>';
			        });
			        tableHtml += '</tr>';
			        return;
			    }

			    // Only create row if it has non-empty values
			    if (row.field && hasNonEmptyValues(data, row.field)) {
			        tableHtml += `<tr><td>${row.label}</td>`;
			        data.forEach(entry => {
			            let value = entry[row.field];
			            // Special handling for boolean values
			            if (row.field === 'visited' || row.field === 'combat') {
			                value = value ? 'Yes' : 'No';
			            }
			            // Don't show 0 values for numeric fields
			            if (typeof value === 'number' && value === 0) {
			                value = '';
			            }
			            tableHtml += `<td style="text-align: center;">${value || ''}</td>`;
			        });
			        tableHtml += '</tr>';
			    }
			});

	                tableHtml += '</table>';
	                $('#search_result_keyword').html(tableHtml);
	            } else {
	                $('#search_result_keyword').html('<p>No results found.</p>');
	            }
	        });
	    });

        // Search notes
        $('#search_button_notes').click(function() {
                let searchNotes = $('#search_notes').val();
                $.get('/search_sleeping_gods_notes', { term: searchNotes }, function(data) {
	        if (data.length > 0) {
                    let tableHtml = '<table id="searchOutput">';   
            
	            // Header row with locations
	                tableHtml += '<tr><th></th>'; // Empty corner cell
	                data.forEach(entry => {
	                    tableHtml += `<th style="text-align: center;">Location ${entry.location}<br>${entry.part || ''}</th>`;
	                });
	                tableHtml += '</tr>';
	    
	                // Fixed rows for each attribute
	                const rows = [
 			    { label: '<h2>Required</h2>'},
	                    { label: 'Keyword', field: 'required_keyword' },
	                    { label: 'Visited', field: 'visited' },
	                    { label: 'Combat Level', field: 'combat_level' },
	                    { label: 'Challenge', field: 'challenge' },
	                    { label: 'Challenge Level', field: 'challenge_level' },
	                    { label: 'Coins', field: 'req_coins' },
	                    { label: 'Meat', field: 'req_meat' },
	                    { label: 'Veg', field: 'req_veg' },
	                    { label: 'Grain', field: 'req_grain' },
	                    { label: 'Materials', field: 'req_wood' },
	                    { label: 'Artifacts', field: 'req_artifacts' },
 			    { label: '<h2>Gained</h2>'},
	                    { label: 'Keyword', field: 'gained_keyword' },
	                    { label: 'Coins', field: 'gain_coins' },
	                    { label: 'Meat', field: 'gain_meat' },
	                    { label: 'Veg', field: 'gain_veg' },
	                    { label: 'Grain', field: 'gain_grain' },
	                    { label: 'Materials', field: 'gain_wood' },
	                    { label: 'Artifacts', field: 'gain_artifacts' },
	                    { label: 'XP', field: 'gain_xp' },
	                    { label: 'Totem', field: 'gain_totem' },
	                    { label: 'Adventure', field: 'gain_adventure' },
	                    { label: 'Ship Damage', field: 'gain_ship_damage' },
	                    { label: 'Ship Repair', field: 'gain_ship_repair' },
	                    { label: 'Crew Damage', field: 'gain_crew_damage' },
	                    { label: 'Crew Health', field: 'gain_crew_health' },
	                    { label: 'Low Morale', field: 'gain_low_morale' },
	                    { label: 'Frightened', field: 'gain_fright' },
	                    { label: 'Venom', field: 'gain_venom' },
	                    { label: 'Weakened', field: 'gain_weakness' },
	                    { label: 'Madness', field: 'gain_madness' },
 			    { label: '<h2>Removed</h2>'},
	                    { label: 'Low Morale', field: 'remove_low_morale' },
	                    { label: 'Frightened', field: 'remove_fright' },
	                    { label: 'Venom', field: 'remove_venom' },
	                    { label: 'Weakened', field: 'remove_weakness' },
	                    { label: 'Madness', field: 'remove_madness' },
			    { label: '<h2>Notes</h2>' }, 
			    { label: 'Notes', field: 'notes' }   
	                ];

			// Create each row
			rows.forEach(row => {
			    // Skip header rows (containing h2 tags)
			    if (row.label.includes('<h2>')) {
			        tableHtml += `<tr><td>${row.label}</td>`;
			        data.forEach(() => {
			            tableHtml += '<td></td>';
			        });
			        tableHtml += '</tr>';
			        return;
			    }

			    // Only create row if it has non-empty values
			    if (row.field && hasNonEmptyValues(data, row.field)) {
			        tableHtml += `<tr><td>${row.label}</td>`;
			        data.forEach(entry => {
			            let value = entry[row.field];
			            // Special handling for boolean values
			            if (row.field === 'visited' || row.field === 'combat') {
			                value = value ? 'Yes' : 'No';
			            }
			            // Don't show 0 values for numeric fields
			            if (typeof value === 'number' && value === 0) {
			                value = '';
			            }
			            tableHtml += `<td style="text-align: center;">${value || ''}</td>`;
			        });
			        tableHtml += '</tr>';
			    }
			});

	                tableHtml += '</table>';
	                $('#search_result_notes').html(tableHtml);
	            } else {
	                $('#search_result_notes').html('<p>No results found.</p>');
	            }
	        });

	    });

        $(document).on('click', '.deleteRow', function () { const rowId = $(this).data('id');
            if (!confirm('Delete this entry permanently?')) return;

                $.ajax({
                    url: '/delete_sleeping_gods_row',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify({ id: rowId }),
                    success: (resp) => {
                        if (resp.success) {
                            // remove the whole column from the table
                            const colIndex = $(this).closest('th').index();
			    $('#searchOutput tr').each(function () {
				$(this).children().eq(colIndex).remove();
			    });
                        } else {
                            alert('Error: ' + (resp.error || 'unknown'));
                        }
                    },
                    error: () => alert('Server error while deleting')
                });
        });

        // Function to convert input to uppercase
        document.getElementById('required_keyword').addEventListener('input', function() {
            this.value = this.value.toUpperCase();
        });
        document.getElementById('gained_keyword').addEventListener('input', function() {
            this.value = this.value.toUpperCase();
        });
        document.getElementById('part').addEventListener('input', function() {
            this.value = this.value.toUpperCase();
        });
        document.getElementById('search_keyword').addEventListener('input', function() {
            this.value = this.value.toUpperCase();
        });

        }); // Closes document ready

        let defeats = localStorage.getItem('defeats') || 0;
        document.getElementById('defeatCount').textContent = defeats;

        function incrementCounter() {
            defeats++;
            localStorage.setItem('defeats', defeats);
            document.getElementById('defeatCount').textContent = defeats;
        }

        function resetCounter() {
            defeats = 0;
            localStorage.setItem('defeats', defeats);
            document.getElementById('defeatCount').textContent = defeats;
        }
	function hasNonEmptyValues(data, field) {
	    return data.some(entry => {
	        let value = entry[field];
	        if (typeof value === 'number') {
	            return value !== 0;
	        }
	        return value && value.toString().trim() !== '';
	    });
	}

    </script>

                     
Board game log screenshot
A screenshot of a list of win/loss statistics for Imperium. This page helps me decide which civilisation pair to attempt next.
Code Snippet: Win / loss statistics for the board game Imperium
                     
@app.route('/imperium')
def imperium():
    conn = get_db_connection()
    cur = conn.cursor()

    #// Abbasids ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Abbasids%' AND result ILIKE '%lost%'
    """)
    abba_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Abbasids%' AND result ILIKE '%won%'
    """)
    abba_won=cur.fetchone()[0]

    #// Aksumites ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Aksumites%' AND result ILIKE '%lost%'
    """)
    aksu_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Aksumites%' AND result ILIKE '%won%'
    """)
    aksu_won=cur.fetchone()[0]

    #// Arthurians ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Arthurians%' AND result ILIKE '%lost%'
    """)
    arth_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Arthurians%' AND result ILIKE '%won%'
    """)
    arth_won=cur.fetchone()[0]

    #// Atlanteans ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Atlant%' AND result ILIKE '%lost%'
    """)
    atlant_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Atlant%' AND result ILIKE '%won%'
    """)
    atlant_won=cur.fetchone()[0]

    #// Carthaginians ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Carthag%' AND result ILIKE '%lost%'
    """)
    carthag_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Carthag%' AND result ILIKE '%won%'
    """)
    carthag_won=cur.fetchone()[0]

    #// Celts ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%celt%' AND result ILIKE '%lost%'
    """)
    celts_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Celt%' AND result ILIKE '%won%'
    """)
    celts_won=cur.fetchone()[0]

    #// Cultists ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%cult%' AND result ILIKE '%lost%'
    """)
    cult_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%cult%' AND result ILIKE '%won%'
    """)
    cult_won=cur.fetchone()[0]

    #// Egyptians ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Egyptians%' AND result ILIKE '%lost%'
    """)
    egyp_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Egyptians%' AND result ILIKE '%won%'
    """)
    egyp_won=cur.fetchone()[0]

    #// Greeks ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Greek%' AND result ILIKE '%lost%'
    """)
    greek_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Greek%' AND result ILIKE '%won%'
    """)
    greek_won=cur.fetchone()[0]

    #// Macedonian ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Macedonian%' AND result ILIKE '%lost%'
    """)
    mace_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Macedonian%' AND result ILIKE '%won%'
    """)
    mace_won=cur.fetchone()[0]

    #// Mauryans ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Mauryans%' AND result ILIKE '%lost%'
    """)
    mau_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Mauryans%' AND result ILIKE '%won%'
    """)
    mau_won=cur.fetchone()[0]

    #// Minoans ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Minoans%' AND result ILIKE '%lost%'
    """)
    min_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Minoans%' AND result ILIKE '%won%'
    """)
    min_won=cur.fetchone()[0]

    #// Olmecs ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Olmecs%' AND result ILIKE '%lost%'
    """)
    olm_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Olmecs%' AND result ILIKE '%won%'
    """)
    olm_won=cur.fetchone()[0]

    #// Persians ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Persian%' AND result ILIKE '%lost%'
    """)
    persian_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Persian%' AND result ILIKE '%won%'
    """)
    persian_won=cur.fetchone()[0]

    #// Qin ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Qin%' AND result ILIKE '%lost%'
    """)
    qin_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Qin%' AND result ILIKE '%won%'
    """)
    qin_won=cur.fetchone()[0]

    #// Scythians ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Scythians%' AND result ILIKE '%lost%'
    """)
    scy_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Scythians%' AND result ILIKE '%won%'
    """)
    scy_won=cur.fetchone()[0]

    #// Utopians ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Utopians%' AND result ILIKE '%lost%'
    """)
    uto_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Utopians%' AND result ILIKE '%won%'
    """)
    uto_won=cur.fetchone()[0]

    #// Vikings ////////////////////////////////////////////////////
    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Viking%' AND result ILIKE '%lost%'
    """)
    vik_lost=cur.fetchone()[0]

    cur.execute("""
        SELECT COUNT(*) 
        FROM imperium 
        WHERE level ILIKE '%Viking%' AND result ILIKE '%won%'
    """)
    vik_won=cur.fetchone()[0]

    return render_template(
        'imperium.html',
	abba_lost=abba_lost,
	abba_won=abba_won,
	aksu_lost=aksu_lost,
	aksu_won=aksu_won,
	arth_lost=arth_lost,
	arth_won=arth_won,
	atlant_lost=atlant_lost,
	atlant_won=atlant_won,
	carthag_lost=carthag_lost,
	carthag_won=carthag_won,
	celts_lost=celts_lost,
	celts_won=celts_won,
	cult_lost=cult_lost,
	cult_won=cult_won,
	egyp_lost=egyp_lost,
	egyp_won=egyp_won,
	greek_lost=greek_lost,
	greek_won=greek_won,
	mace_lost=mace_lost,
	mace_won=mace_won,
	mau_lost=mau_lost,
	mau_won=mau_won,
	min_lost=min_lost,
	min_won=min_won,
	olm_lost=olm_lost,
	olm_won=olm_won,
	persian_lost=persian_lost,
	persian_won=persian_won,
	qin_lost=qin_lost,
	qin_won=qin_won,
	scy_lost=scy_lost,
	scy_won=scy_won,
	uto_lost=uto_lost,
	uto_won=uto_won,
	vik_lost=vik_lost,
	vik_won=vik_won
    )

                     
                     <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Imperium</title>
    <!-- Your styles here -->
</head>
<body>
    <h1>Imperium Stats Playing as Romans</h1>
    <button onclick="window.location.href='/'">Back to Home</button>

    <table style="border-collapse: separate; border-spacing: 20px 0;">
    <table>
	<tr>
            <th style="padding: 10px; text-align: left;"></th>
            <th style="padding: 10px; text-align: left;">Times Lost</th>
            <th style="padding: 10px; text-align: left;">Times Won</th>
	</tr>
	<tr>
            <th style="padding: 10px; text-align: left;"></th>
	</tr>
	<tr>
            <th style="padding: 10px; text-align: left;">Classics</th>
	</tr>
	<tr>
            <td style="padding: 10px; ">Carthaginians &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Celts &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Greeks &#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Macedonians &#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Persians &#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Scythians &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Vikings &#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <th style="padding: 10px; text-align: left;"></th>
	</tr>
	<tr>
            <th style="padding: 10px; text-align: left;">Legends</th>
	</tr>
	<tr>
            <td style="padding: 10px; ">Arthurians &#9733&#9733&#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Atlanteans &#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Egyptians &#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Mauryans &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Minoans &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Olmecs &#9733&#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Qin &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Utopians &#9733&#9733&#9733&#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <th style="padding: 10px; text-align: left;"></th>
	</tr>
	<tr>
            <th style="padding: 10px; text-align: left;">Horizons</th>
	</tr>
            <td style="padding: 10px; ">Abbasids &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Aksumites &#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
	<tr>
            <td style="padding: 10px; ">Cultists &#9733&#9733&#9733&#9733&#9733&#9733 </td>
            <td style="padding: 10px; text-align: right;"></td>
            <td style="padding: 10px; text-align: right;"></td>
	</tr>
    </table>
</body>
</html>