commit c4d72ba7bb45fb4d3c7517026252cd137fcca8ed
parent 1864d2082f14e064d2bd027ce86902e22e3b00fe
Author: alex wennerberg <alex@alexwennerberg.com>
Date: Mon, 6 Oct 2025 14:31:34 -0400
python rewrite
Diffstat:
22 files changed, 222 insertions(+), 281 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -6,3 +6,4 @@ allcards.txt
.venv
__pycache__
_site
+venv/
diff --git a/Gemfile.lock b/Gemfile.lock
@@ -1,59 +0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- base64 (0.2.0)
- logger (1.6.4)
- mustermann (3.0.3)
- ruby2_keywords (~> 0.0.1)
- nio4r (2.7.4)
- puma (6.5.0)
- nio4r (~> 2.0)
- rack (3.1.8)
- rack-protection (4.1.1)
- base64 (>= 0.1.0)
- logger (>= 1.6.0)
- rack (>= 3.0.0, < 4)
- rack-session (2.0.0)
- rack (>= 3.0.0)
- rackup (2.2.1)
- rack (>= 3)
- ruby2_keywords (0.0.5)
- sinatra (4.1.1)
- logger (>= 1.6.0)
- mustermann (~> 3.0)
- rack (>= 3.0.0, < 4)
- rack-protection (= 4.1.1)
- rack-session (>= 2.0.0, < 3)
- tilt (~> 2.0)
- sqlite3 (2.4.1-aarch64-linux-gnu)
- sqlite3 (2.4.1-aarch64-linux-musl)
- sqlite3 (2.4.1-arm-linux-gnu)
- sqlite3 (2.4.1-arm-linux-musl)
- sqlite3 (2.4.1-arm64-darwin)
- sqlite3 (2.4.1-x86-linux-gnu)
- sqlite3 (2.4.1-x86-linux-musl)
- sqlite3 (2.4.1-x86_64-darwin)
- sqlite3 (2.4.1-x86_64-linux-gnu)
- sqlite3 (2.4.1-x86_64-linux-musl)
- tilt (2.5.0)
-
-PLATFORMS
- aarch64-linux-gnu
- aarch64-linux-musl
- arm-linux-gnu
- arm-linux-musl
- arm64-darwin
- x86-linux-gnu
- x86-linux-musl
- x86_64-darwin
- x86_64-linux-gnu
- x86_64-linux-musl
-
-DEPENDENCIES
- puma
- rackup
- sinatra
- sqlite3
-
-BUNDLED WITH
- 2.5.9
diff --git a/app.py b/app.py
@@ -0,0 +1,101 @@
+from flask import Flask, render_template, request
+import sqlite3
+from urllib.parse import quote as url_quote
+
+app = Flask(__name__)
+
+def get_db():
+ db = sqlite3.connect('3cb.db')
+ db.row_factory = sqlite3.Row
+ return db
+
+@app.route('/')
+def index():
+ db = get_db()
+ rounds = db.execute("select r.id, r.fileid, r.date, count(distinct d.player) as player_count from round r left join deck d on r.id = d.round group by r.id, r.fileid, r.date order by r.id desc").fetchall()
+ db.close()
+ subtitle = "rounds"
+ return render_template('index.html', rounds=rounds, subtitle=subtitle)
+
+@app.route('/round/<int:id>')
+def round_detail(id):
+ db = get_db()
+ round_data = db.execute("select * from round where id = ?", (id,)).fetchone()
+ matches = db.execute("select * from round_score where round = ? order by final_score desc,prelim_score desc", (id,)).fetchall()
+ banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
+ db.close()
+ subtitle = "round " + str(id)
+ return render_template('matches.html', round=round_data, matches=matches, banned_cards=banned_cards, subtitle=subtitle)
+
+@app.route('/card')
+def card():
+ name = request.args.get('name')
+ db = get_db()
+ matches = db.execute("select * from round_score where card1 = ?1 or card2 = ?1 or card3 = ?1 order by round desc", (name,)).fetchall()
+ banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
+ db.close()
+ subtitle = name
+ return render_template('matches.html', matches=matches, banned_cards=banned_cards, subtitle=subtitle)
+
+@app.route('/player')
+def player():
+ name = request.args.get('name')
+ db = get_db()
+ matches = db.execute("select * from round_score where player = ? order by round desc", (name,)).fetchall()
+ banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
+ db.close()
+ subtitle = "player " + name
+ return render_template('matches.html', matches=matches, banned_cards=banned_cards, subtitle=subtitle)
+
+@app.route('/deck')
+def deck():
+ cards = sorted([request.args.get('c1'), request.args.get('c2'), request.args.get('c3')])
+ db = get_db()
+ matches = db.execute("""
+ select * from round_score where
+ card1 = ?1 and card2 = ?2 and card3 = ?3 or
+ card1 = ?1 and card2 = ?3 and card3 = ?2 or
+ card1 = ?2 and card2 = ?1 and card3 = ?3 or
+ card1 = ?2 and card2 = ?3 and card3 = ?1 or
+ card1 = ?3 and card2 = ?1 and card3 = ?2 or
+ card1 = ?3 and card2 = ?2 and card3 = ?1
+ order by round desc""", (cards[0], cards[1], cards[2])).fetchall()
+ banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
+ db.close()
+ subtitle = "; ".join(cards)
+ return render_template('matches.html', matches=matches, banned_cards=banned_cards, subtitle=subtitle)
+
+@app.route('/players')
+def players():
+ db = get_db()
+ players = db.execute("""
+ select
+ d.player,
+ count(distinct d.round) as rounds_played,
+ sum(case when r.rank = 1 then 1 else 0 end) as rounds_won,
+ min(ro.date) as first_played
+ from deck d
+ left join rank r on d.round = r.round and d.player = r.player
+ left join round ro on d.round = ro.id
+ group by d.player
+ order by rounds_played desc
+ """).fetchall()
+ db.close()
+ subtitle = "players"
+ return render_template('players.html', players=players, subtitle=subtitle)
+
+@app.route('/banned')
+def banned():
+ db = get_db()
+ banned_cards = db.execute("select name from ban order by name").fetchall()
+ db.close()
+ subtitle = "banned cards"
+ return render_template('banned.html', banned_cards=banned_cards, subtitle=subtitle)
+
+# Template filter for url encoding
+@app.template_filter('url_encode')
+def url_encode_filter(s):
+ return url_quote(str(s))
+
+if __name__ == '__main__':
+ app.run(debug=True)
+\ No newline at end of file
diff --git a/deploy.sh b/deploy.sh
@@ -0,0 +1,3 @@
+scp main.rb alpine@fishbb.org:~
+scp -r views alpine@fishbb.org:~
+ssh alpine@fishbb.org "doas rc-service 3cm restart"
diff --git a/main.rb b/main.rb
@@ -1,81 +0,0 @@
-require 'sinatra'
-require 'sqlite3'
-require 'erb'
-include ERB::Util
-
-db = SQLite3::Database.new "3cb.db"
-db.results_as_hash = true
-set :server_settings, :timeout => 5
-
-get '/' do
- @rounds = db.execute "select r.id, r.fileid, r.date, count(distinct d.player) as player_count from round r left join deck d on r.id = d.round group by r.id, r.fileid, r.date order by r.id desc"
- @subtitle = "rounds"
- erb :index
-end
-
-get '/round/:id' do |id|
- @round = db.execute("select * from round where id = ?", id)[0]
- @matches = db.execute "select * from round_score where round = ? order by final_score desc,prelim_score desc", id
- @banned_cards = db.execute("select name from ban").map { |row| row["name"] }
- @subtitle = "round " + id
- erb :matches
-end
-
-get '/card' do
- name = params[:name]
- @matches = db.execute "select * from round_score where
- card1 = ?1 or
- card2 = ?1 or
- card3 = ?1 order by round desc", name
- @banned_cards = db.execute("select name from ban").map { |row| row["name"] }
- @subtitle = name
- erb :matches
-end
-
-get '/player' do
- name = params[:name]
- @matches = db.execute "select * from round_score where player = ? order by round desc", name
- @banned_cards = db.execute("select name from ban").map { |row| row["name"] }
- @subtitle = "player " + name
- erb :matches # rename round -> matches
-end
-
-get '/deck' do
- cards = [params[:c1], params[:c2], params[:c3]].sort
- # i am lazy
- @matches = db.execute("
- select * from round_score where
- card1 = ?1 and card2 = ?2 and card3 = ?3 or
- card1 = ?1 and card2 = ?3 and card3 = ?2 or
- card1 = ?2 and card2 = ?1 and card3 = ?3 or
- card1 = ?2 and card2 = ?3 and card3 = ?1 or
- card1 = ?3 and card2 = ?1 and card3 = ?2 or
- card1 = ?3 and card2 = ?2 and card3 = ?1
- order by round desc", [cards[0], cards[1], cards[2]])
- @banned_cards = db.execute("select name from ban").map { |row| row["name"] }
- @subtitle = cards.join("; ")
- erb :matches
-end
-
-get '/players' do
- @players = db.execute "
- select
- d.player,
- count(distinct d.round) as rounds_played,
- sum(case when r.rank = 1 then 1 else 0 end) as rounds_won,
- min(ro.date) as first_played
- from deck d
- left join rank r on d.round = r.round and d.player = r.player
- left join round ro on d.round = ro.id
- group by d.player
- order by rounds_played desc
- "
- @subtitle = "players"
- erb :players
-end
-
-get '/banned' do
- @banned_cards = db.execute("select name from ban order by name")
- @subtitle = "banned cards"
- erb :banned
-end
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1 @@
+Flask==2.3.3
+\ No newline at end of file
diff --git a/public/style.css b/static/style.css
diff --git a/templates/banned.html b/templates/banned.html
@@ -0,0 +1,18 @@
+{% include 'header.html' %}
+
+<div class="banned-cards-grid">
+{% for card in banned_cards %}
+ <div class="banned-card">
+ <div class="card-image">
+ <img src="https://gatherer.wizards.com/Handlers/Image.ashx?name={{ card.name|url_encode }}&type=card"
+ alt="{{ card.name }}"
+ onerror="this.src='https://cards.scryfall.io/normal/front/0/0/00000000-0000-0000-0000-000000000000.jpg'">
+ </div>
+ <div class="card-name">
+ <a href="/card?name={{ card.name|url_encode }}">{{ card.name }}</a>
+ </div>
+ </div>
+{% endfor %}
+</div>
+
+{% include 'footer.html' %}
+\ No newline at end of file
diff --git a/templates/footer.html b/templates/footer.html
@@ -0,0 +1,6 @@
+<footer>
+ Made by aw for <a href="https://sites.google.com/view/3cb-metashape/home">3 Card Blind Metashape</a>. <a href="https://git.sr.ht/~aw/3cb-data">Source</a>
+</footer>
+</main>
+</body>
+</html>
+\ No newline at end of file
diff --git a/templates/header.html b/templates/header.html
@@ -0,0 +1,14 @@
+<html>
+<html dir="ltr" lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<meta name="description" content="Data analysis for 3 card blind metashape" />
+<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
+<title>3 card blind data analysis</title>
+</head>
+<body>
+ <header>
+ <b><a href="/">3cb-data</a></b> > <b>{{ subtitle }}</b>
+ </header>
+<main>
+\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
@@ -0,0 +1,20 @@
+{% include 'header.html' %}
+<p><a href="/players">View all players</a> | <a href="/banned">View banned cards</a></p>
+<form action="/card" method="get" class="card-search">
+ <input type="text" name="name" placeholder="Search for a card..." required>
+ <input type="submit" value="Search">
+</form>
+<div class="rounds-list">
+{% for round in rounds %}
+ <div class="round-item">
+ <a href="/round/{{ round.id }}">Round {{ "%02d"|format(round.id) }}</a>
+ <span class="round-meta">
+ {% if round.date %}
+ {{ round.date }}
+ {% endif %}
+[{{ round.player_count }} players]
+ </span>
+ </div>
+{% endfor %}
+</div>
+{% include 'footer.html' %}
+\ No newline at end of file
diff --git a/templates/matches.html b/templates/matches.html
@@ -0,0 +1,28 @@
+{% include 'header.html' %}
+ {% if round %}
+ <a href='https://docs.google.com/spreadsheets/d/{{ round.fileid }}/'>Google Sheet</a>{% endif %}
+{% if not matches %}
+ <p>This card has never been played, or may be spelled wrong!</p>
+{% else %}
+<div class="flextable">
+ {% for match in matches %}
+ <div class="deck-box"
+ {% if match.rank == 1 %} style="background:#c5ffc5" {% elif match.final_score %} style="background:#ffffc5"{% endif %}>
+ <div style="width:160px">
+ <b><a href='/player?name={{ match.player }}'>{{ match.player }}</a></b><br>
+ <a href="/round/{{ match.round }}">Round {{ match.round }}</a><br>
+ Rank: {{ match.rank }}<br>
+ Groups{{ match.group_name }}: {{ match.prelim_score }}
+{% if match.final_score %}<br>
+ Finals: {{ match.final_score }}
+{% endif %}<br>
+ <a href="/deck?c1={{ match.card1 }}&c2={{ match.card2 }}&c3={{ match.card3 }}">deck</a><br>
+</div>
+<div>
+ {% for card in [match.card1, match.card2, match.card3] %}<div style="position: relative; display: inline-block;"><a href="/card?name={{ card }}"><img alt='{{ card }}' width=146 height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact={{ card|url_encode }}&format=image&version=medium" /></a>{% if card in banned_cards %}<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.7); color: white; font-weight: bold; font-size: 20px; padding: 5px 10px; border-radius: 5px;">BANNED</div>{% endif %}</div>{% endfor %}
+</div>
+</div>
+{% endfor %}
+</div>
+{% endif %}
+{% include 'footer.html' %}
+\ No newline at end of file
diff --git a/templates/players.html b/templates/players.html
@@ -0,0 +1,22 @@
+{% include 'header.html' %}
+<table class="players-table">
+ <thead>
+ <tr>
+ <th>Player</th>
+ <th>Rounds</th>
+ <th>Wins</th>
+ <th>First Played</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for player in players %}
+ <tr>
+ <td><a href="/player?name={{ player.player|url_encode }}">{{ player.player }}</a></td>
+ <td>{{ player.rounds_played }}</td>
+ <td>{{ player.rounds_won }}</td>
+ <td>{{ player.first_played }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% include 'footer.html' %}
+\ No newline at end of file
diff --git a/views/card.erb b/views/card.erb
@@ -1,3 +0,0 @@
-<%= erb :header %>
-<%= erb :footer%>
-
diff --git a/views/footer.erb b/views/footer.erb
@@ -1,6 +0,0 @@
-<footer>
- Made by aw for <a href="https://sites.google.com/view/3cb-metashape/home">3 Card Blind Metashape</a>. <a href="https://git.sr.ht/~aw/3cb-data">Source</a>
-</footer>
-</main>
-</body>
-</html>
diff --git a/views/header.erb b/views/header.erb
@@ -1,14 +0,0 @@
-<html>
-<html dir="ltr" lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1" />
-<meta name="description" content="Data analysis for 3 card blind metashape" />
-<link rel="stylesheet" type="text/css" href="/style.css">
-<title>3 card blind data analysis</title>
-</head>
-<body>
- <header>
- <b><a href="/">3cb-data</a></b> > <b><%= @subtitle %></b>
- </header>
-<main>
diff --git a/views/index.erb b/views/index.erb
@@ -1,20 +0,0 @@
-<%= erb :header %>
-<p><a href="/players">View all players</a> | <a href="/banned">View banned cards</a></p>
-<form action="/card" method="get" class="card-search">
- <input type="text" name="name" placeholder="Search for a card..." required>
- <input type="submit" value="Search">
-</form>
-<div class="rounds-list">
-<% @rounds.each do |round| %>
- <div class="round-item">
- <a href="/round/<%= round["id"] %>">Round <%= round["id"].to_s.rjust(2, "0") %></a>
- <span class="round-meta">
- <% if round["date"] %>
- <%= round["date"] %>
- <% end %>
-[<%= round["player_count"] %> players]
- </span>
- </div>
-<% end %>
-</div>
-<%= erb :footer %>
diff --git a/views/matches.erb b/views/matches.erb
@@ -1,28 +0,0 @@
-<%= erb :header %>
- <% if @round %>
- <a href='https://docs.google.com/spreadsheets/d/<%= @round["fileid"] %>/'>Google Sheet</a><% end %>
-<% if @matches.empty? %>
- <p>This card has never been played, or may be spelled wrong!</p>
-<% else %>
-<div class="flextable">
- <% @matches.each do |match| %>
- <div class="deck-box"
- <% if match["rank"] == 1 %> style="background:#c5ffc5" <% elsif match["final_score"] %> style="background:#ffffc5"<% end %>>
- <div style="width:160px">
- <b><a href='/player?name=<%= match["player"] %>'><%= match["player"] %></a></b><br>
- <a href="/round/<%= match["round"]%>">Round <%= match["round"] %></a><br>
- Rank: <%= match["rank"] %><br>
- Groups<%= match["group_name"] %>: <%= match["prelim_score"] %>
-<% if match["final_score"] %><br>
- Finals: <%= match["final_score"] %>
-<% end %><br>
- <a href="/deck?c1=<%= match["card1"] %>&c2=<%= match["card2"] %>&c3=<%= match["card3"]%>">deck</a><br>
-</div>
-<div>
- <% [match["card1"], match["card2"], match["card3"]].each do |card| %><div style="position: relative; display: inline-block;"><a href="/card?name=<%= card %>"><img alt='<%= card %>' width=146 height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact=<%= url_encode(card) %>&format=image&version=medium" /></a><% if @banned_cards.include?(card) %><div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.7); color: white; font-weight: bold; font-size: 20px; padding: 5px 10px; border-radius: 5px;">BANNED</div><% end %></div><% end %>
-</div>
-</div>
-<% end %>
-</div>
-<% end %>
-<%= erb :footer %>
diff --git a/views/player.erb b/views/player.erb
@@ -1,15 +0,0 @@
-<%= erb :header %>
-Submissions:
-<table>
-<% @matches.each do |match| %>
- <tr>
- <td><%= match["round"] %></td>
- <td><%= match["prelim_score"] %></td>
- <td><%= match["final_score"] %></td>
- <td><%= match["card1"] %></td>
- <td><%= match["card2"] %></td>
- <td><%= match["card3"] %></td>
- </tr>
-<% end %>
-</table>
-<%= erb :footer%>
diff --git a/views/players.erb b/views/players.erb
@@ -1,22 +0,0 @@
-<%= erb :header %>
-<table class="players-table">
- <thead>
- <tr>
- <th>Player</th>
- <th>Rounds</th>
- <th>Wins</th>
- <th>First Played</th>
- </tr>
- </thead>
- <tbody>
- <% @players.each do |player| %>
- <tr>
- <td><a href="/player?name=<%= url_encode(player["player"]) %>"><%= player["player"] %></a></td>
- <td><%= player["rounds_played"] %></td>
- <td><%= player["rounds_won"] %></td>
- <td><%= player["first_played"] %></td>
- </tr>
- <% end %>
- </tbody>
-</table>
-<%= erb :footer %>
-\ No newline at end of file
diff --git a/views/round.erb b/views/round.erb
@@ -1,21 +0,0 @@
-<%= erb :header %>
- <% if @round %>
- <a href='https://docs.google.com/spreadsheets/d/<%= @round["fileid"] %>/'>Google Sheet</a><% end %>
-<div class="flextable">
- <% @matches.each do |match| %>
- <div class="deck-box"
- <% if match["final_score"] %> style="background:#ffffc5"<% end %>>
- <div style="width:160px">
- <b><a href='/player/<%= match["player"] %>'><%= match["player"] %></a></b><br>
- <a href="/round/<%= match["round"]%>"><%= match["round"] %></a>
- <a href="/deck?card=<%= match["card1"] %>&card=<%= match["card2"] %>card= <%= match["card3"]%>>deck</a><br>
- Group <%= match["prelim_group"] %>: <%=match["prelim_score"]%><br>
- <% if match["final_score"] %>Finals: <%= match["final_score"] %><% end %>
-</div>
-<div>
- <% [match["card1"], match["card2"], match["card3"]].each do |card| %><img alt='<%= card %>' width=146height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact=<%= card %>&format=image&version=small" /><% end %>
-</div>
-</div>
-<% end %>
-</div>
-<%= erb :footer %>
diff --git a/views/submission.erb b/views/submission.erb
@@ -1,11 +0,0 @@
-<div class="deck-match">
- <div>
- playername<br>
- Group A: 43<br>
- Finals: 10<br>
- </div>
- <div>
- card123
- deck link
- </div>
-</div>