a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh

feat: add submissions and make src dir

dunkirk.sh bc2272fb d3602e5c

verified
Changed files
+151 -4
internal
runner
server
storage
battleship-arena

This is a binary file and will not be displayed.

+3
internal/runner/runner.go
···
buildDir := filepath.Join(enginePath, "build")
os.MkdirAll(buildDir, 0755)
+
+
srcDir := filepath.Join(enginePath, "src")
+
os.MkdirAll(srcDir, 0755)
srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename)
dstPath := filepath.Join(enginePath, "src", sub.Filename)
+69 -2
internal/server/users.go
···
import (
"fmt"
"html/template"
+
"log"
"net/http"
"strings"
···
}
}
+
// Get user's submissions with stats
+
submissions, err := storage.GetUserSubmissionsWithStats(username)
+
if err != nil {
+
log.Printf("Error getting submissions for %s: %v", username, err)
+
submissions = []storage.SubmissionWithStats{}
+
}
+
if submissions == nil {
+
submissions = []storage.SubmissionWithStats{}
+
}
+
log.Printf("Found %d submissions for %s", len(submissions), username)
+
// Parse public key for display
publicKeyDisplay := formatPublicKey(user.PublicKey)
tmpl := template.Must(template.New("user").Parse(userProfileHTML))
data := struct {
-
User *storage.User
-
Entry *storage.LeaderboardEntry
+
User *storage.User
+
Entry *storage.LeaderboardEntry
+
Submissions []storage.SubmissionWithStats
PublicKeyDisplay string
}{
User: user,
Entry: userEntry,
+
Submissions: submissions,
PublicKeyDisplay: publicKeyDisplay,
}
tmpl.Execute(w, data)
···
<div class="stat-card">
<div class="stat-label">Win Rate</div>
<div class="stat-value">{{printf "%.1f" .Entry.WinPct}}%</div>
+
</div>
+
</div>
+
{{end}}
+
+
{{if .Submissions}}
+
<div class="key-section" style="margin-bottom: 2rem;">
+
<h2 class="section-title">📤 Submissions</h2>
+
<div style="overflow-x: auto;">
+
<table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">
+
<thead>
+
<tr style="border-bottom: 1px solid #334155;">
+
<th style="text-align: left; padding: 0.75rem 0.5rem; color: #94a3b8;">Filename</th>
+
<th style="text-align: left; padding: 0.75rem 0.5rem; color: #94a3b8;">Uploaded</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Rating</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Wins</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Losses</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Win Rate</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Avg Moves</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Status</th>
+
<th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Active</th>
+
</tr>
+
</thead>
+
<tbody>
+
{{range .Submissions}}
+
<tr style="border-bottom: 1px solid #334155;">
+
<td style="padding: 0.75rem 0.5rem; font-family: Monaco, monospace;">{{.Filename}}</td>
+
<td style="padding: 0.75rem 0.5rem; color: #94a3b8;">{{.UploadTime.Format "Jan 2, 3:04 PM"}}</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">
+
{{if .HasMatches}}{{.Rating}} <span style="color: #94a3b8; font-size: 0.8em;">±{{.RD}}</span>{{else}}-{{end}}
+
</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{.Wins}}{{else}}-{{end}}</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{.Losses}}{{else}}-{{end}}</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">
+
{{if .HasMatches}}
+
{{if ge .WinPct 60.0}}<span style="color: #10b981;">{{printf "%.1f" .WinPct}}%</span>{{end}}
+
{{if and (lt .WinPct 60.0) (ge .WinPct 40.0)}}<span style="color: #f59e0b;">{{printf "%.1f" .WinPct}}%</span>{{end}}
+
{{if lt .WinPct 40.0}}<span style="color: #ef4444;">{{printf "%.1f" .WinPct}}%</span>{{end}}
+
{{else}}-{{end}}
+
</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{printf "%.1f" .AvgMoves}}{{else}}-{{end}}</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">
+
{{if eq .Status "completed"}}<span style="color: #10b981;">✓</span>{{end}}
+
{{if eq .Status "pending"}}<span style="color: #fbbf24;">⏳</span>{{end}}
+
{{if eq .Status "testing"}}<span style="color: #3b82f6;">⚙️</span>{{end}}
+
{{if eq .Status "failed"}}<span style="color: #ef4444;">✗</span>{{end}}
+
</td>
+
<td style="padding: 0.75rem 0.5rem; text-align: center;">
+
{{if .IsActive}}<span style="color: #10b981;">●</span>{{else}}<span style="color: #64748b;">○</span>{{end}}
+
</td>
+
</tr>
+
{{end}}
+
</tbody>
+
</table>
</div>
</div>
{{end}}
+79 -2
internal/storage/database.go
···
Filename string
UploadTime time.Time
Status string
+
IsActive bool
+
}
+
+
type SubmissionWithStats struct {
+
Submission
+
Rating int
+
RD int
+
Wins int
+
Losses int
+
WinPct float64
+
AvgMoves float64
+
LastPlayed time.Time
+
HasMatches bool
}
type Tournament struct {
···
func GetUserSubmissions(username string) ([]Submission, error) {
rows, err := DB.Query(
-
"SELECT id, username, filename, upload_time, status FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10",
+
"SELECT id, username, filename, upload_time, status, is_active FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10",
username,
)
if err != nil {
···
var submissions []Submission
for rows.Next() {
var s Submission
-
err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status)
+
err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status, &s.IsActive)
if err != nil {
return nil, err
}
+
submissions = append(submissions, s)
+
}
+
+
return submissions, rows.Err()
+
}
+
+
func GetUserSubmissionsWithStats(username string) ([]SubmissionWithStats, error) {
+
query := `
+
SELECT
+
s.id,
+
s.username,
+
s.filename,
+
s.upload_time,
+
s.status,
+
s.is_active,
+
COALESCE(s.glicko_rating, 1500.0) as rating,
+
COALESCE(s.glicko_rd, 350.0) as rd,
+
COALESCE(SUM(CASE WHEN m.player1_id = s.id THEN m.player1_wins WHEN m.player2_id = s.id THEN m.player2_wins ELSE 0 END), 0) as total_wins,
+
COALESCE(SUM(CASE WHEN m.player1_id = s.id THEN m.player2_wins WHEN m.player2_id = s.id THEN m.player1_wins ELSE 0 END), 0) as total_losses,
+
COALESCE(AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END), 0) as avg_moves,
+
MAX(m.timestamp) as last_played,
+
COUNT(m.id) as match_count
+
FROM submissions s
+
LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1
+
WHERE s.username = ?
+
GROUP BY s.id, s.username, s.filename, s.upload_time, s.status, s.is_active, s.glicko_rating, s.glicko_rd
+
ORDER BY s.upload_time DESC
+
LIMIT 10
+
`
+
+
rows, err := DB.Query(query, username)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
var submissions []SubmissionWithStats
+
for rows.Next() {
+
var s SubmissionWithStats
+
var lastPlayed *string
+
var rating, rd float64
+
var matchCount int
+
+
err := rows.Scan(
+
&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status, &s.IsActive,
+
&rating, &rd, &s.Wins, &s.Losses, &s.AvgMoves, &lastPlayed, &matchCount,
+
)
+
if err != nil {
+
return nil, err
+
}
+
+
s.Rating = int(rating)
+
s.RD = int(rd)
+
s.HasMatches = matchCount > 0
+
+
totalGames := s.Wins + s.Losses
+
if totalGames > 0 {
+
s.WinPct = float64(s.Wins) / float64(totalGames) * 100.0
+
}
+
+
if lastPlayed != nil {
+
s.LastPlayed, _ = time.Parse("2006-01-02 15:04:05", *lastPlayed)
+
}
+
submissions = append(submissions, s)
}