Skip to content

Commit 33800b4

Browse files
committed
feat(app): fallacies bingo
1 parent 6dc72e8 commit 33800b4

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

public/apps/apps.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
"created_at": "2025-12-20T12:00:00+03:00",
1515
"pinned_order": 5
1616
},
17+
{
18+
"slug": "logical-fallacy-bingo",
19+
"to": "/apps/logical-fallacy-bingo",
20+
"title": "Fallacy Bingo",
21+
"description": "A playable bingo game based on common logical fallacies.",
22+
"icon": "BrainIcon",
23+
"created_at": "2026-02-28T12:00:00+03:00"
24+
},
1725
{
1826
"slug": "feztype",
1927
"to": "/apps/feztype",
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
ArrowsClockwiseIcon,
6+
BrainIcon,
7+
CheckCircleIcon
8+
} from '@phosphor-icons/react';
9+
import Seo from '../../components/Seo';
10+
import BreadcrumbTitle from '../../components/BreadcrumbTitle';
11+
12+
const FALLACIES = [
13+
{ name: 'Ad Hominem', desc: 'Attacking the person instead of the argument.' },
14+
{ name: 'Straw Man', desc: 'Misrepresenting an argument to make it easier to attack.' },
15+
{ name: 'Slippery Slope', desc: 'Assuming a small step leads to a chain of extreme events.' },
16+
{ name: 'False Dilemma', desc: 'Presenting only two options when more exist.' },
17+
{ name: 'Post Hoc', desc: 'Assuming A caused B because A happened first.' },
18+
{ name: 'Whataboutism', desc: 'Deflecting by bringing up a different issue (Tu Quoque).' },
19+
{ name: 'Red Herring', desc: 'Introducing an irrelevant topic to distract.' },
20+
{ name: 'Confirmation Bias', desc: 'Favoring info that confirms existing beliefs.' },
21+
{ name: 'Sunk Cost', desc: 'Continuing because of past investment, not future value.' },
22+
{ name: 'Dunning-Kruger', desc: 'Overestimating ability when knowledge is low.' },
23+
{ name: 'No True Scotsman', desc: 'Changing the definition to exclude counter-examples.' },
24+
{ name: 'Texas Sharpshooter', desc: 'Cherry-picking data to fit a pattern.' },
25+
{ name: 'Moving Goalposts', desc: 'Changing the criteria for proof after evidence is met.' },
26+
{ name: 'Begging the Question', desc: 'The premise assumes the conclusion is true.' },
27+
{ name: 'Appeal to Authority', desc: 'Saying it is true because an "expert" said so.' },
28+
{ name: 'Appeal to Emotion', desc: 'Manipulating emotions instead of using logic.' },
29+
{ name: 'Bandwagon', desc: 'Saying it is true because many people believe it.' },
30+
{ name: 'Appeal to Ignorance', desc: 'Assuming true because not proven false (or vice versa).' },
31+
{ name: 'Burden of Proof', desc: 'Making a claim but expecting others to disprove it.' },
32+
{ name: 'Personal Incredulity', desc: 'Saying it is false because it is hard to understand.' },
33+
{ name: 'Ambiguity', desc: 'Using double meanings to mislead or misrepresent the truth.' },
34+
{ name: 'Genetic Fallacy', desc: 'Judging something based on where it comes from.' },
35+
{ name: 'Middle Ground', desc: 'Assuming a compromise between two extremes is the truth.' },
36+
{ name: 'Anecdotal', desc: 'Using personal experience instead of sound evidence.' }
37+
];
38+
39+
function LogicalFallaciesBingoPage() {
40+
const [grid, setGrid] = useState([]);
41+
const [marked, setMarked] = useState(new Set());
42+
const [bingo, setBingo] = useState(false);
43+
44+
const shuffleAndGenerate = () => {
45+
const shuffled = [...FALLACIES].sort(() => Math.random() - 0.5);
46+
const newGrid = [];
47+
let index = 0;
48+
for (let i = 0; i < 25; i++) {
49+
if (i === 12) {
50+
newGrid.push({ name: 'FREE SPACE', desc: 'You are using the internet', isFree: true });
51+
} else {
52+
newGrid.push(shuffled[index]);
53+
index++;
54+
}
55+
}
56+
setGrid(newGrid);
57+
setMarked(new Set([12])); // Free space marked
58+
setBingo(false);
59+
};
60+
61+
useEffect(() => {
62+
shuffleAndGenerate();
63+
}, []);
64+
65+
const toggleMark = (index) => {
66+
if (index === 12) return; // Can't unmark free space
67+
const newMarked = new Set(marked);
68+
if (newMarked.has(index)) {
69+
newMarked.delete(index);
70+
} else {
71+
newMarked.add(index);
72+
}
73+
setMarked(newMarked);
74+
checkBingo(newMarked);
75+
};
76+
77+
const checkBingo = (currentMarked) => {
78+
const winningLines = [
79+
[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24], // Rows
80+
[0, 5, 10, 15, 20], [1, 6, 11, 16, 21], [2, 7, 12, 17, 22], [3, 8, 13, 18, 23], [4, 9, 14, 19, 24], // Cols
81+
[0, 6, 12, 18, 24], [4, 8, 12, 16, 20] // Diagonals
82+
];
83+
84+
const isBingo = winningLines.some(line => line.every(index => currentMarked.has(index)));
85+
setBingo(isBingo);
86+
};
87+
88+
return (
89+
<div className="min-h-screen bg-[#050505] text-white selection:bg-emerald-500/30 font-sans">
90+
<Seo
91+
title="Logical Fallacies Bingo | Fezcodex"
92+
description="A playable bingo game based on common logical fallacies found on the internet."
93+
keywords={['bingo', 'logical fallacies', 'game', 'logic', 'internet arguments']}
94+
/>
95+
<div className="mx-auto max-w-5xl px-6 py-24 md:px-12">
96+
<header className="mb-12">
97+
<Link
98+
to="/apps"
99+
className="group mb-12 inline-flex items-center gap-2 text-xs font-mono text-gray-500 hover:text-white transition-colors uppercase tracking-[0.3em]"
100+
>
101+
<ArrowLeftIcon
102+
weight="bold"
103+
className="transition-transform group-hover:-translate-x-1"
104+
/>
105+
<span>Applications</span>
106+
</Link>
107+
108+
<div className="flex flex-col md:flex-row md:items-end justify-between gap-8">
109+
<div className="space-y-4">
110+
<BreadcrumbTitle
111+
title="Fallacy Bingo"
112+
slug="fb"
113+
variant="brutalist"
114+
/>
115+
<p className="text-xl text-gray-400 max-w-2xl font-light leading-relaxed">
116+
Surviving a family dinner or a Twitter thread? Keep track of the terrible arguments you encounter with this interactive bingo board. Need a refresher? Read the <Link to="/posts/encyclopedia-of-bad-arguments" className="text-emerald-400 hover:text-emerald-300 underline decoration-emerald-500/30 underline-offset-4">Encyclopedia of Bad Arguments</Link>.
117+
</p>
118+
</div>
119+
120+
<button
121+
onClick={shuffleAndGenerate}
122+
className="group relative inline-flex items-center gap-4 px-8 py-4 bg-white text-black hover:bg-emerald-400 transition-all duration-300 font-mono uppercase tracking-widest text-sm font-black rounded-sm shrink-0"
123+
>
124+
<ArrowsClockwiseIcon weight="bold" size={20} className={bingo ? "animate-spin" : ""} />
125+
<span>Shuffle Board</span>
126+
</button>
127+
</div>
128+
</header>
129+
130+
{bingo && (
131+
<div className="mb-8 p-6 bg-emerald-500/20 border border-emerald-500/50 rounded-sm flex items-center gap-4 text-emerald-400 animate-pulse">
132+
<CheckCircleIcon size={32} weight="fill" />
133+
<div>
134+
<h2 className="text-xl font-bold uppercase tracking-widest font-mono">Bingo!</h2>
135+
<p className="text-sm opacity-80">You've successfully witnessed a complete collapse of logic.</p>
136+
</div>
137+
</div>
138+
)}
139+
140+
<div className="grid grid-cols-5 gap-2 md:gap-4 w-full aspect-square max-w-4xl mx-auto">
141+
{grid.map((cell, index) => {
142+
const isMarked = marked.has(index);
143+
const isFree = cell.isFree;
144+
return (
145+
<button
146+
key={index}
147+
onClick={() => toggleMark(index)}
148+
className={`
149+
relative flex flex-col items-center justify-center p-2 md:p-4 text-center border transition-all duration-300 rounded-sm
150+
${isMarked
151+
? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-400 shadow-[inset_0_0_20px_rgba(16,185,129,0.2)]'
152+
: 'bg-white/[0.02] border-white/10 hover:border-white/30 hover:bg-white/[0.05] text-gray-300'
153+
}
154+
${isFree ? 'bg-white/5 border-white/20' : ''}
155+
`}
156+
>
157+
<span className="font-mono text-[9px] md:text-xs font-bold uppercase tracking-wider mb-1 md:mb-2 leading-tight">
158+
{cell.name}
159+
</span>
160+
{!isFree && (
161+
<span className="hidden md:block text-[10px] opacity-60 leading-tight">
162+
{cell.desc}
163+
</span>
164+
)}
165+
{isMarked && !isFree && (
166+
<div className="absolute top-2 right-2 text-emerald-500">
167+
<CheckCircleIcon weight="fill" size={16} />
168+
</div>
169+
)}
170+
{isFree && (
171+
<BrainIcon className="mt-2 text-emerald-500/50" size={24} />
172+
)}
173+
</button>
174+
);
175+
})}
176+
</div>
177+
178+
<footer className="mt-24 pt-12 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-6 text-gray-600 font-mono text-[10px] uppercase tracking-[0.3em]">
179+
<span>Fezcodex_Logic_Defense_Module_v1.0</span>
180+
<span className="text-gray-800">READY</span>
181+
</footer>
182+
</div>
183+
</div>
184+
);
185+
}
186+
187+
export default LogicalFallaciesBingoPage;

0 commit comments

Comments
 (0)