Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions firestore.rules
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
rules_version = '2';

// Cloud saves (src/cloud.ts, src/firebase.ts): each player's save is one document, saves/{uid}, which only that
// signed-in player can read or write. The scoreboard (src/scores.ts) is one document per player, scores/{uid}: anyone
// can read it, a page of at most 50 at a time, and only that signed-in player can write it. The wall of shame
// (src/wallOfShame.ts) is the same, shame/{uid}: each player's worst bet, only ever swapped for a worse one. A bug
// signed-in player can read or write; it also says whether they keep off the scoreboards (`hidden`). The scoreboard
// (src/scores.ts) is one document per player, scores/{uid}: anyone can read it, a page of at most 50 at a time, and
// only that signed-in player can write it, or delete it to come off. The wall of shame (src/wallOfShame.ts) is the
// same, shame/{uid}: each player's worst bet, only ever swapped for a worse one, or deleted by them. A bug
// report (src/report.ts) is a document in reports/, which a player signed in with Google can add but no player can
// read, change or delete; the intake workflow (intake/) reads them with its own service account, which these rules
// don't apply to. Each report goes with an update to limits/{uid}, which counts a player's reports: up to 20 in an
Expand All @@ -15,10 +16,11 @@ service cloud.firestore {
match /saves/{uid} {
allow read: if request.auth != null && request.auth.uid == uid;
allow create, update: if request.auth != null && request.auth.uid == uid
&& request.resource.data.keys().hasOnly(['data', 'savedAt', 'updated'])
&& request.resource.data.keys().hasOnly(['data', 'savedAt', 'updated', 'hidden'])
&& request.resource.data.data is string
&& request.resource.data.data.size() < 100000
&& request.resource.data.savedAt is number;
&& request.resource.data.savedAt is number
&& (!('hidden' in request.resource.data) || request.resource.data.hidden is bool);
}
match /scores/{uid} {
allow get: if true;
Expand All @@ -30,6 +32,7 @@ service cloud.firestore {
&& request.resource.data.best is number
&& request.resource.data.best >= 0
&& request.resource.data.stage in [1, 2, 3];
allow delete: if request.auth != null && request.auth.uid == uid;
}
match /shame/{uid} {
allow get: if true;
Expand All @@ -45,6 +48,7 @@ service cloud.firestore {
&& (!('on' in request.resource.data) || str(request.resource.data.on, 12))
&& request.resource.data.broke is bool
&& request.resource.data.at is number;
allow delete: if request.auth != null && request.auth.uid == uid;
}

function googleUser(uid) {
Expand Down
66 changes: 47 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -449,28 +449,38 @@
/* the Ko-fi link, to buy the maker a coffee: a settings row like the others, opening Ko-fi in a window of its own (kofi.ts) */
#kofi{ box-sizing:border-box; text-decoration:none; }
#kofi i{ margin-left:auto; font-style:normal; opacity:.6; }
/* the privacy notice (public/privacy.html), in a tab of its own: in every build, Steam's too */
#privacy{ box-sizing:border-box; text-decoration:none; }
#privacy i{ margin-left:auto; font-style:normal; opacity:.6; }
/* Privacy, in every build (Steam's too): signed in, the switch for showing on the scoreboards; the notice's short
version; and a button for the whole notice
(public/privacy.html), which opens in a dialog of its own over the game (settings.ts) */
#settings #privacyCard{ color:var(--pill-ink); font-size:12px; line-height:1.4; font-weight:500; }
#privacyCard ul{ margin:0; padding-left:16px; }
#privacyCard li{ margin:2px 0; }
#privacyCard a{ color:inherit; }
#privacyCard a:focus-visible{ outline:3px solid var(--coral); outline-offset:2px; }
#privacyDialog{ padding-top:max(44px, calc(env(safe-area-inset-top,0px) + 38px)); }
#privacyDialog > .k-sheet{ width:min(100%,760px); height:100%; padding:14px; }
#privacyDialog .k-title{ padding:4px 44px 0; }
#privacyDialog iframe{ flex:1; min-height:0; width:100%; border:0; border-radius:16px; }
/* the categories (Graphics, Sound, Credits & Copyrights): a row with a chevron, opening into a panel under it */
#settings .cat .chev{ margin:0 4px 4px auto; width:9px; height:9px; flex:none; border-right:2.5px solid currentColor; border-bottom:2.5px solid currentColor; transform:rotate(45deg); transition:transform .2s; }
#settings .cat[aria-expanded="true"] .chev{ transform:translateY(4px) rotate(-135deg); }
#settings .catPanel{ display:flex; flex-direction:column; gap:8px; padding:8px 10px 10px; border-radius:14px; background:var(--chip); }
#settings .catPanel[hidden]{ display:none; }
/* Controls opens into How to play (the controls card, hint.ts) and a switch for the Buy button on a computer */
#controls{ display:flex; align-items:center; min-height:40px; padding:4px 12px; border:0; border-radius:12px; background:var(--pill); color:var(--pill-ink); font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; text-align:left; cursor:pointer; box-shadow:0 2px 0 var(--shade); }
#controls i{ margin-left:auto; font-style:normal; font-size:20px; line-height:1; opacity:.6; }
#controlsPanel .toggle{ display:flex; align-items:center; gap:10px; padding:2px 2px 0 4px; font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; }
#controlsPanel .toggle > span{ display:flex; flex-direction:column; line-height:1.15; min-width:0; }
#buyBtnNote{ font-size:12px; opacity:.6; }
#buyBtnNote:empty{ display:none; }
#buyBtnToggle{ position:relative; flex:none; margin-left:auto; width:52px; height:30px; padding:0; border:0; border-radius:999px; background:rgba(127,140,150,.45); cursor:pointer; transition:background .15s; }
#buyBtnToggle::after{ content:""; position:absolute; top:3px; left:3px; width:24px; height:24px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.3); transition:transform .15s; }
#buyBtnToggle[aria-checked="true"]{ background:var(--coral); }
#buyBtnToggle[aria-checked="true"]::after{ transform:translateX(22px); }
#buyBtnToggle:disabled{ opacity:.55; cursor:default; }
#controls:focus-visible, #buyBtnToggle:focus-visible{ outline:3px solid var(--coral); outline-offset:2px; }
@media (prefers-reduced-motion: reduce){ #buyBtnToggle, #buyBtnToggle::after{ transition:none; } }
/* Controls opens into How to play (the controls card, hint.ts) and a switch for the Buy button on a computer; Privacy
holds one for showing on the scoreboards (cloud.ts) */
#controls, #privacyFull{ display:flex; align-items:center; min-height:40px; padding:4px 12px; border:0; border-radius:12px; background:var(--pill); color:var(--pill-ink); font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; text-align:left; cursor:pointer; box-shadow:0 2px 0 var(--shade); }
#controls i, #privacyFull i{ margin-left:auto; font-style:normal; font-size:20px; line-height:1; opacity:.6; }
#settings .toggle{ display:flex; align-items:center; gap:10px; padding:2px 2px 0 4px; font:700 15px "Floe Emoji","Baloo 2","Trebuchet MS",system-ui,sans-serif; }
#settings .toggle > span{ display:flex; flex-direction:column; line-height:1.15; min-width:0; }
#settings .toggle small{ font-size:12px; opacity:.6; }
#settings .toggle small:empty, #settings .toggle[hidden]{ display:none; }
#settings .switch{ position:relative; flex:none; margin:0 0 0 auto; width:52px; height:30px; min-height:0; padding:0; border:0; border-radius:999px; background:rgba(127,140,150,.45); cursor:pointer; transition:background .15s; }
#settings .switch::after{ content:""; position:absolute; top:3px; left:3px; width:24px; height:24px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.3); transition:transform .15s; }
#settings .switch[aria-checked="true"]{ background:var(--coral); }
#settings .switch[aria-checked="true"]::after{ transform:translateX(22px); }
#settings .switch:disabled{ opacity:.55; cursor:default; }
#controls:focus-visible, #privacyFull:focus-visible, #settings .switch:focus-visible{ outline:3px solid var(--coral); outline-offset:2px; }
@media (prefers-reduced-motion: reduce){ #settings .switch, #settings .switch::after{ transition:none; } }
/* Sound opens into a mute button and a 0-10 slider for each kind of sound, sized for fingers */
/* Report a bug or idea (report.ts): the settings row (signed out, it signs in instead) opens a window of its own over
the dimmed game, with a note, the picture of the game, a tickbox each for the picture and the game state, and a
Expand Down Expand Up @@ -1500,13 +1510,26 @@ <h2 class="sgroup">Game</h2>
<button id="controlsCat" class="cat" type="button" aria-expanded="false" aria-controls="controlsPanel"><span class="ic" aria-hidden="true">🕹️</span>Controls<i class="chev" aria-hidden="true"></i></button>
<div id="controlsPanel" class="catPanel" role="group" aria-label="Controls" hidden>
<button id="controls" type="button">How to play<i aria-hidden="true">›</i></button>
<div class="toggle"><span><span id="buyBtnName">Buy button on screen</span><small id="buyBtnNote"></small></span><button id="buyBtnToggle" type="button" role="switch" aria-checked="true" aria-labelledby="buyBtnName" aria-describedby="buyBtnNote"></button></div>
<div class="toggle"><span><span id="buyBtnName">Buy button on screen</span><small id="buyBtnNote"></small></span><button id="buyBtnToggle" class="switch" type="button" role="switch" aria-checked="true" aria-labelledby="buyBtnName" aria-describedby="buyBtnNote"></button></div>
</div>
<button id="restart" type="button"><span class="ic" aria-hidden="true">↺</span><span id="restartText">Restart</span></button>
<h2 class="sgroup">About</h2>
<button id="reportCat" type="button" aria-haspopup="dialog" hidden><span class="ic" aria-hidden="true">🐞</span><span id="reportName">Report a bug or idea</span><i aria-hidden="true">›</i></button>
<a id="kofi" href="https://ko-fi.com/chefcontrol" target="_blank" rel="noopener"><span class="ic" aria-hidden="true">☕</span>Buy me a coffee<i aria-hidden="true">↗</i></a>
<a id="privacy" href="privacy.html" target="_blank" rel="noopener"><span class="ic" aria-hidden="true">🛡️</span>Privacy notice<i aria-hidden="true">↗</i></a>
<button id="privacyBtn" class="cat" type="button" aria-expanded="false" aria-controls="privacyCard"><span class="ic" aria-hidden="true">🛡️</span>Privacy<i class="chev" aria-hidden="true"></i></button>
<div id="privacyCard" class="catPanel" role="region" aria-labelledby="privacyBtn" hidden>
<!-- signed in only (cloud.ts) -->
<div class="toggle" id="boards" hidden><span><span id="boardsName">Show me on the scoreboards</span><small id="boardsNote"></small></span><button id="boardsToggle" class="switch" type="button" role="switch" aria-checked="true" aria-labelledby="boardsName" aria-describedby="boardsNote"></button></div>
<!-- the notice's short version, word for word (public/privacy.html; test/ui.test.ts keeps them the same) -->
<ul>
<li><b>Web game, not signed in:</b> your game stays on your device.</li>
<li><b>Web game, signed in with Google:</b> your save, your first name and initial on the scoreboards (you can turn that off), and any report you send are kept with Google Firebase.</li>
<li><b>Steam version:</b> the game sends nothing to me. Steam keeps your saves and scores.</li>
<li>No ads, no analytics, no tracking. I don't sell your data.</li>
<li>To see or delete your data, email <a href="mailto:oran.dan@outlook.com">oran.dan@outlook.com</a>.</li>
</ul>
<button id="privacyFull" type="button" aria-haspopup="dialog">Read the full notice<i aria-hidden="true">›</i></button>
</div>
<button id="creditsBtn" class="cat" type="button" aria-expanded="false" aria-controls="creditsCard"><span class="ic" aria-hidden="true">©</span>Credits &amp; Copyrights<i class="chev" aria-hidden="true"></i></button>
<div id="creditsCard" class="catPanel" role="region" aria-labelledby="creditsBtn" hidden>
<h3>Music</h3>
Expand All @@ -1533,6 +1556,11 @@ <h3>The ring's crowd</h3>
</ul>
</div>
</section>
<div id="privacyDialog" role="dialog" aria-modal="true" aria-labelledby="privacyTitle" hidden><div class="k-sheet">
<button type="button" id="privacyClose" class="k-btn k-plain k-x" aria-label="Close the privacy notice"><span aria-hidden="true">✕</span></button>
<h2 id="privacyTitle" class="k-title">🛡️ Privacy notice</h2>
<iframe id="privacyFrame" title="Floe Market privacy notice"></iframe>
</div></div>
<div id="report" role="dialog" aria-modal="true" aria-labelledby="reportTitle" hidden><div class="card">
<h2 id="reportTitle">Report a bug or idea</h2>
<button type="button" id="reportClose" aria-label="Close the report"><span aria-hidden="true">✕</span></button>
Expand Down
31 changes: 21 additions & 10 deletions public/privacy.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,22 @@
.back{ max-width:720px; margin:0 auto 12px; font:800 16px var(--sans); }
.back a{ color:var(--ink); text-decoration:none; }
.back a:hover{ text-decoration:underline; }
/* inside the game's own dialog (Settings, Privacy): just the card, filling it */
.framed body{ padding:0; background:var(--pill); }
.framed main{ max-width:none; border-radius:0; box-shadow:none; }
</style>
</head>
<body>
<p class="back"><a href="./">Play Floe Market ›</a></p>
<main>
<h1>Floe Market privacy notice</h1>
<p class="meta">Last changed 27 October 2026. This covers the web game and the Steam version.</p>
<p class="meta">Last changed 28 October 2026. This covers the web game and the Steam version.</p>

<section class="short" aria-labelledby="short">
<h2 id="short">The short version</h2>
<ul>
<li><b>Web game, not signed in:</b> your game stays on your device.</li>
<li><b>Web game, signed in with Google:</b> your save, your name on the scoreboards, and any report you send are kept with Google Firebase.</li>
<li><b>Web game, signed in with Google:</b> your save, your first name and initial on the scoreboards (you can turn that off), and any report you send are kept with Google Firebase.</li>
<li><b>Steam version:</b> the game sends nothing to me. Steam keeps your saves and scores.</li>
<li>No ads, no analytics, no tracking. I don't sell your data.</li>
<li>To see or delete your data, email <a href="mailto:oran.dan@outlook.com">oran.dan@outlook.com</a>.</li>
Expand All @@ -81,28 +84,28 @@ <h3>If you don't sign in</h3>
</ul>

<h3>If you sign in with Google</h3>
<p>Signing in turns on cloud saves, the scoreboard, the wall of shame and reports. This is what's kept:</p>
<p>Signing in turns on cloud saves, the scoreboard, the wall of shame and reports. To stay off the scoreboard and the wall of shame, open Settings, open Privacy and turn off <i>Show me on the scoreboards</i>. That takes your entries down at once and puts nothing new up, on every device you sign in on. Turning it back on puts you back. This is what's kept:</p>
<div class="items">
<div class="item"><b>Your sign-in</b><dl>
<dt>What</dt><dd>An account number, your Google name, email address and profile picture link, and when you signed in.</dd>
<dt>Why</dt><dd>To know which save is yours.</dd>
<dt>Who sees it</dt><dd>Me, and Google, who runs Firebase. Never other players.</dd>
<dt>Kept</dt><dd>Until you ask me to delete it.</dd></dl></div>
<div class="item"><b>Your save</b><dl>
<dt>What</dt><dd>Your game: progress, money, upgrades, look and settings, and when you saved. It syncs every 30 seconds while you play.</dd>
<dt>What</dt><dd>Your game: progress, money, upgrades, look and settings, and when you saved. It syncs every 30 seconds while you play. It also keeps whether you show on the scoreboards.</dd>
<dt>Why</dt><dd>So your game follows you to any device.</dd>
<dt>Who sees it</dt><dd>Only you. I can see it as the owner of the project, but I don't look.</dd>
<dt>Kept</dt><dd>Until you ask me to delete it.</dd></dl></div>
<div class="item"><b>🏆 The scoreboard</b><dl>
<dt>What</dt><dd>Your name, the most cash you've held at once, and the furthest stage you've reached.</dd>
<dt>What</dt><dd>Your first name and the first letter of your last name, the most cash you've held at once, and the furthest stage you've reached.</dd>
<dt>Why</dt><dd>To show the best players.</dd>
<dt>Who sees it</dt><dd><b>Everyone who plays.</b> It shows your Google name. If that name looks like an email address, it shows “A player” instead.</dd>
<dt>Kept</dt><dd>Until you ask me to delete it.</dd></dl></div>
<dt>Who sees it</dt><dd><b>Everyone who plays.</b> It shows your Google name cut short, so “Pat Smith” shows as “Pat S.”. If that name looks like an email address, it shows “A player” instead.</dd>
<dt>Kept</dt><dd>Until you turn off <i>Show me on the scoreboards</i>, which deletes it at once, or ask me to delete it.</dd></dl></div>
<div class="item"><b>💔 The wall of shame</b><dl>
<dt>What</dt><dd>Your name, the most you've lost in one round at the casino, which game, whether it left you broke, and when.</dd>
<dt>What</dt><dd>Your first name and the first letter of your last name, the most you've lost in one round at the casino, which game, whether it left you broke, and when.</dd>
<dt>Why</dt><dd>For fun, in the casino's bar.</dd>
<dt>Who sees it</dt><dd><b>Everyone who plays</b>, the same way as the scoreboard.</dd>
<dt>Kept</dt><dd>Until you ask me to delete it.</dd></dl></div>
<dt>Kept</dt><dd>The same as the scoreboard: until you turn off <i>Show me on the scoreboards</i>, or ask me to delete it.</dd></dl></div>
<div class="item"><b>Your report count</b><dl>
<dt>What</dt><dd>How many reports you've sent in the last hour.</dd>
<dt>Why</dt><dd>To stop more than 20 reports an hour.</dd>
Expand Down Expand Up @@ -153,7 +156,7 @@ <h2>Your rights</h2>
<ul>
<li>show you the data I have about you, and send you a copy;</li>
<li>correct it;</li>
<li>delete it: your sign-in, save, scoreboard and wall of shame entries, report count and reports;</li>
<li>delete it: your sign-in, save, scoreboard and wall of shame entries, report count and reports (you can take your scoreboard and wall of shame entries down yourself, with <i>Show me on the scoreboards</i> under Privacy in Settings);</li>
<li>stop using it, or use it less.</li>
</ul>
<p><b>How to ask:</b> email <a href="mailto:oran.dan@outlook.com">oran.dan@outlook.com</a>. For the web game, say which Google account you sign in with. I answer within a month.</p>
Expand All @@ -166,5 +169,13 @@ <h2>Children</h2>
<h2>Changes to this notice</h2>
<p>When this notice changes, the date at the top changes too. Every older version stays in the game's public code on <a href="https://github.com/ChefControl/floe-market/commits/main/public/privacy.html">GitHub</a>.</p>
</main>
<script>
// Opened inside the game (Settings, Privacy): the game's already there behind it, and a link opens in a tab of its own
if (window.self !== window.top) {
document.documentElement.classList.add('framed');
document.querySelector('.back')?.remove();
for (const a of document.querySelectorAll('a[href^="http"]')) { a.target = '_blank'; a.rel = 'noopener'; }
}
</script>
</body>
</html>
Loading
Loading