Skip to content

Instantly share code, notes, and snippets.

@taksvj
Created August 4, 2025 08:24
Show Gist options
  • Save taksvj/7d620aa254acdf853216ebb4b714611b to your computer and use it in GitHub Desktop.
Save taksvj/7d620aa254acdf853216ebb4b714611b to your computer and use it in GitHub Desktop.
Untitled
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EthOS $AIR Perpetual Game Preview</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body {
margin: 0;
background: #c0c0e0 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVQoU2P8z/D/PwMDAwNjAwMDAwMDIxsDAwMDP8AAGqQSuAAAAAElFTkSuQmCC') repeat;
font-family: 'Courier New', monospace;
}
.App {
color: #000000;
min-height: 100vh;
text-align: center;
padding: 20px 0;
}
h1 {
font-size: 1.8rem;
margin-bottom: 0.5em;
font-weight: bold;
color: #000080;
background: #c0c0c0;
padding: 5px;
border: 2px outset #ffffff;
display: inline-block;
}
.balance {
font-size: 1rem;
margin-bottom: 0.5em;
background: #c0c0c0;
padding: 5px;
border: 2px outset #ffffff;
display: inline-block;
}
.current-price {
font-size: 1.1rem;
margin-bottom: 0.5em;
font-weight: bold;
background: #c0c0c0;
padding: 5px;
border: 2px outset #ffffff;
display: inline-block;
}
.profit-display {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.5em;
background: #c0c0c0;
padding: 5px;
border: 2px outset #ffffff;
display: inline-block;
}
.round-info {
font-size: 0.9rem;
margin-bottom: 0.5em;
background: #c0c0c0;
padding: 5px;
border: 2px outset #ffffff;
display: inline-block;
}
.controls {
margin: 10px 0;
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.bet-btn {
font-size: 0.9rem;
padding: 0.4em 1em;
margin: 0 0.1em;
border: 2px outset #ffffff;
background: #808080;
color: #000000;
cursor: pointer;
font-weight: bold;
transition: border 0.2s;
}
.bet-btn:disabled {
background: #a0a0a0;
color: #404040;
cursor: default;
border: 2px solid #808080;
}
.bet-btn.up.selected,
.bet-btn.up:hover:not(:disabled) {
background: #00ff00;
color: #000000;
border: 2px outset #ffffff;
}
.bet-btn.down.selected,
.bet-btn.down:hover:not(:disabled) {
background: #ff0000;
color: #000000;
border: 2px outset #ffffff;
}
.start-btn {
font-size: 0.9rem;
background: #00ff00;
color: #000000;
padding: 0.4em 1.2em;
border: 2px outset #ffffff;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.start-btn:disabled {
background: #a0a0a0;
color: #404040;
border: 2px solid #808080;
cursor: default;
}
.cashout-btn {
font-size: 0.9rem;
background: #ffff00;
color: #000000;
padding: 0.4em 1.2em;
border: 2px outset #ffffff;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.cashout-btn:hover {
background: #ffff80;
}
input[type="number"] {
font-size: 0.9rem;
padding: 0.3em 0.5em;
border: 2px inset #808080;
background: #ffffff;
color: #000000;
font-family: 'Courier New', monospace;
}
.history {
max-width: 480px;
margin: 20px auto 0 auto;
background: #c0c0c0;
border: 2px outset #ffffff;
padding: 10px;
}
.history h3 {
margin-bottom: 0.5em;
color: #000080;
font-weight: bold;
}
.history table {
width: 100%;
border-collapse: collapse;
}
.history th, .history td {
font-size: 0.8rem;
padding: 0.2em 0.3em;
border: 1px solid #808080;
}
.history th {
background: #000080;
color: #ffffff;
font-weight: bold;
}
.history td {
background: #ffffff;
color: #000000;
}
.modal-backdrop {
position: fixed;
left: 0; top: 0; right: 0; bottom: 0;
background: #00000080;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: #c0c0c0;
color: #000000;
border: 2px outset #ffffff;
padding: 20px;
box-shadow: 0 4px 32px #00000040;
min-width: 260px;
}
.modal h2 {
font-size: 1.5rem;
margin-bottom: 0.5em;
color: #000080;
}
.modal button {
font-size: 0.9rem;
background: #00ff00;
color: #000000;
padding: 0.4em 1.2em;
border: 2px outset #ffffff;
font-weight: bold;
cursor: pointer;
}
.tab {
background: #c0c0c0;
border: 2px outset #ffffff;
padding: 5px 10px;
margin: 5px;
display: inline-block;
cursor: default;
}
.tab-content {
background: #ffffff;
border: 2px inset #808080;
padding: 10px;
margin-top: 5px;
display: none;
}
.tab-content.active {
display: block;
}
/* Animasi untuk Chart */
.chart-path {
animation: draw 1s linear forwards;
}
@keyframes draw {
from {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
}
to {
stroke-dasharray: 1000;
stroke-dashoffset: 0;
}
}
</style>
</head>
<body>
<div id="root"></div>
<!-- Audio Files -->
<audio id="startSound" src="https://www.soundjay.com/buttons/beep-01a.mp3"></audio>
<audio id="cashoutSound" src="https://www.soundjay.com/buttons/coin-01a.mp3"></audio>
<audio id="endSound" src="https://www.soundjay.com/buttons/beep-03.mp3"></audio>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// KOMPONEN CHART
function Chart({ priceSeries }) {
const width = 300;
const height = 100;
if (priceSeries.length < 2) return <div style={{ height, width, border: "2px inset #808080", background: "#ffffff", marginBottom: 10, margin: 'auto' }} />;
const min = Math.min(...priceSeries);
const max = Math.max(...priceSeries);
const range = max - min || 1;
const points = priceSeries.map((p, i) => [
(i / (priceSeries.length - 1)) * width,
height - ((p - min) / range) * (height - 10) - 5,
]);
const path = points
.map((pt, i) => (i === 0 ? `M${pt[0]},${pt[1]}` : `L${pt[0]},${pt[1]}`))
.join(" ");
const totalLength = points.length * 10; // Estimasi panjang garis
return (
<svg
width={width}
height={height}
style={{
border: "2px inset #808080",
background: "#ffffff",
marginBottom: 10,
}}
>
<path
className="chart-path"
d={path}
fill="none"
stroke="#000080"
strokeWidth={2}
strokeLinejoin="round"
style={{ strokeDasharray: totalLength, strokeDashoffset: totalLength }}
/>
</svg>
);
}
// KOMPONEN CONTROLS
function Controls({ bet, setBet, betAmount, setBetAmount, canBet, onStart, onCashOut, inRound, balance }) {
return (
<div className="controls">
<div>
<button
className={`bet-btn up ${bet === "UP" ? "selected" : ""}`}
disabled={inRound}
onClick={() => setBet("UP")}
aria-label="Bet on price going up"
>
UP
</button>
<button
className={`bet-btn down ${bet === "DOWN" ? "selected" : ""}`}
disabled={inRound}
onClick={() => setBet("DOWN")}
aria-label="Bet on price going down"
>
DOWN
</button>
</div>
<div>
<input
type="number"
min={1}
max={balance}
value={betAmount}
disabled={inRound}
onChange={e => {
const value = Math.max(1, Math.min(balance, Number(e.target.value) || 1));
setBetAmount(value);
}}
style={{ width: 80, marginRight: 8 }}
aria-label="Bet amount in USD"
/> USD
</div>
{inRound ? (
<button className="cashout-btn" onClick={onCashOut} aria-label="Cash out current bet">
Cash Out
</button>
) : (
<button className="start-btn" onClick={onStart} disabled={!canBet} aria-label="Start the betting round">
Start
</button>
)}
</div>
);
}
// KOMPONEN HISTORY
function History({ history }) {
if (!history.length) return null;
return (
<div className="history">
<h3>History</h3>
<table>
<thead>
<tr>
<th>Round</th>
<th>Bet</th>
<th>Amount</th>
<th>Start</th>
<th>End</th>
<th>P/L</th>
</tr>
</thead>
<tbody>
{history.map((h) => (
<tr key={h.round}>
<td>{h.round}</td>
<td>{h.bet}</td>
<td>${h.betAmount}</td>
<td>${h.priceStart.toFixed(4)}</td>
<td>${h.priceEnd.toFixed(4)}</td>
<td style={{ color: h.profit > 0 ? "#00ff00" : h.profit < 0 ? "#ff0000" : "#000000" }}>
{h.profit > 0 ? "+" : ""}
{h.profit.toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// KOMPONEN RESULT MODAL
function ResultModal({ open, result, priceStart, priceEnd, bet, betAmount, onClose }) {
if (!open) return null;
return (
<div className="modal-backdrop">
<div className="modal" role="dialog" aria-labelledby="modal-title">
<h2 id="modal-title">{result === "WIN" ? "🎉 You Win!" : result === "LOSE" ? "😔 You Lose" : "😐 Draw"}</h2>
<div>
Bet: <strong>{bet}</strong>
</div>
<div>
Bet Amount: <strong>${betAmount}</strong>
</div>
<div>
Start Price: <strong>${priceStart?.toFixed(4)}</strong>
</div>
<div>
End Price: <strong>${priceEnd?.toFixed(4)}</strong>
</div>
<button onClick={onClose} aria-label="Proceed to next round">Next Round</button>
</div>
</div>
);
}
// KOMPONEN UTAMA (APP)
function App() {
const START_BALANCE = 100;
const ROUND_DURATION = 15;
const [balance, setBalance] = useState(START_BALANCE);
const [currentPrice, setCurrentPrice] = useState(0.007);
const [priceSeries, setPriceSeries] = useState([0.007]);
const [bet, setBet] = useState(null);
const [betAmount, setBetAmount] = useState(10);
const [round, setRound] = useState(1);
const [timer, setTimer] = useState(ROUND_DURATION);
const [inRound, setInRound] = useState(false);
const [priceStart, setPriceStart] = useState(null);
const [priceEnd, setPriceEnd] = useState(null);
const [showResult, setShowResult] = useState(false);
const [result, setResult] = useState(null);
const [currentProfit, setCurrentProfit] = useState(0);
const [history, setHistory] = useState([]);
const intervalRef = useRef(null);
const timerRef = useRef(null);
const getRandomPrice = (base) => {
return +(base + (Math.random() - 0.5) * 0.0005).toFixed(4);
};
useEffect(() => {
if (!inRound) return;
intervalRef.current = setInterval(() => {
setCurrentPrice((last) => {
const next = getRandomPrice(last);
setPriceSeries((ps) => [...ps.slice(-29), next].slice(-30));
if (bet && priceStart !== null) {
let profit = -betAmount;
const priceDiff = next - priceStart;
if ((bet === "UP" && priceDiff > 0) || (bet === "DOWN" && priceDiff < 0)) {
profit = betAmount * Math.min(2, Math.abs(priceDiff) / priceStart);
} else if (priceDiff === 0) {
profit = 0;
}
setCurrentProfit(profit);
}
return next;
});
}, 700);
return () => clearInterval(intervalRef.current);
}, [inRound, bet, priceStart, betAmount]);
useEffect(() => {
if (!inRound) return;
if (timer === 0) {
endRound(false);
return;
}
timerRef.current = setTimeout(() => setTimer((s) => s - 1), 1000);
return () => clearTimeout(timerRef.current);
}, [timer, inRound]);
const startRound = () => {
if (inRound || !bet || betAmount <= 0 || betAmount > balance) return;
setBalance(b => b - betAmount);
setInRound(true);
setTimer(ROUND_DURATION);
setPriceStart(currentPrice);
setPriceEnd(null);
setPriceSeries([currentPrice]);
setShowResult(false);
setResult(null);
setCurrentProfit(0);
document.getElementById('startSound').play(); // Putar suara saat ronde dimulai
};
const endRound = (isCashOut) => {
setInRound(false);
clearTimeout(timerRef.current);
const finalPrice = currentPrice;
setPriceEnd(finalPrice);
let finalProfit = 0;
if (isCashOut) {
finalProfit = currentProfit;
document.getElementById('cashoutSound').play(); // Putar suara saat cash out
} else {
const priceDiff = finalPrice - priceStart;
if ((bet === "UP" && priceDiff > 0) || (bet === "DOWN" && priceDiff < 0)) {
finalProfit = betAmount;
} else if (priceDiff === 0) {
finalProfit = 0;
} else {
finalProfit = -betAmount;
}
}
const totalReturn = betAmount + finalProfit;
if (totalReturn > 0) {
setBalance(b => b + totalReturn);
}
setResult(finalProfit > 0 ? "WIN" : finalProfit < 0 ? "LOSE" : "DRAW");
setShowResult(true);
document.getElementById('endSound').play(); // Putar suara saat ronde berakhir
setHistory((h) => [
{
round,
bet,
betAmount,
outcome: finalProfit > 0 ? "WIN" : finalProfit < 0 ? "LOSE" : "DRAW",
priceStart,
priceEnd: finalPrice,
profit: finalProfit,
},
...h.slice(0, 9),
]);
setRound((r) => r + 1);
};
const handleCashOut = () => {
if (!inRound) return;
endRound(true);
};
const handleNext = () => {
setShowResult(false);
setBet(null);
setBetAmount(10);
setPriceSeries([currentPrice]);
setPriceStart(null);
setPriceEnd(null);
setCurrentProfit(0);
};
return (
<div className="App">
<div className="tab">EthOS $AIR Perpetual</div>
<div className="tab-content active">
<div className="balance">Balance: ${balance.toFixed(2)}</div>
<Chart priceSeries={priceSeries} />
<div className="current-price">Current Price: ${currentPrice.toFixed(4)}</div>
{inRound && (
<div className="profit-display" style={{ color: currentProfit >= 0 ? '#00ff00' : '#ff0000' }}>
P/L: {currentProfit >= 0 ? '+' : ''}${currentProfit.toFixed(2)}
</div>
)}
<div className="round-info">
Round: {round} {inRound && `(Ends in ${timer}s)`}
</div>
<Controls
bet={bet}
setBet={setBet}
betAmount={betAmount}
setBetAmount={setBetAmount}
canBet={!inRound && betAmount > 0 && betAmount <= balance && bet !== null}
onStart={startRound}
onCashOut={handleCashOut}
inRound={inRound}
balance={balance}
/>
<History history={history} />
<ResultModal
open={showResult}
result={result}
priceStart={priceStart}
priceEnd={priceEnd}
bet={bet}
betAmount={betAmount}
onClose={handleNext}
/>
</div>
</div>
);
}
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment