Created
December 30, 2025 02:13
-
-
Save wwtv127/9922c598b009490ce325a4882e0d71ad to your computer and use it in GitHub Desktop.
R1 Artifact: My Project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>R1 Uno</title> | |
| <style> | |
| :root { | |
| --r1-bg: #000000; | |
| --r1-orange: #ff4e00; | |
| --card-red: #ea3323; | |
| --card-blue: #0045ad; | |
| --card-green: #33a532; | |
| --card-yellow: #fecb00; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| -webkit-tap-highlight-color: transparent; | |
| user-select: none; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background-color: var(--r1-bg); | |
| color: white; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| width: 240px; | |
| height: 292px; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #game-container { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 5px; | |
| } | |
| /* Opponent Area */ | |
| #opponent-info { | |
| height: 40px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 8px; | |
| padding: 0 10px; | |
| font-size: 12px; | |
| } | |
| /* Play Area */ | |
| #play-area { | |
| flex: 1; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 15px; | |
| } | |
| .card { | |
| width: 50px; | |
| height: 75px; | |
| border-radius: 6px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-weight: bold; | |
| font-size: 20px; | |
| border: 2px solid white; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.5); | |
| position: relative; | |
| } | |
| .card-red { background-color: var(--card-red); } | |
| .card-blue { background-color: var(--card-blue); } | |
| .card-green { background-color: var(--card-green); } | |
| .card-yellow { background-color: var(--card-yellow); } | |
| .card-wild { background-color: #111; border-color: #555; } | |
| .deck { | |
| background: linear-gradient(135deg, #222, #444); | |
| border: 2px solid #666; | |
| cursor: pointer; | |
| } | |
| /* Player Hand */ | |
| #player-hand-container { | |
| height: 100px; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| white-space: nowrap; | |
| padding: 5px 0; | |
| scrollbar-width: none; | |
| } | |
| #player-hand-container::-webkit-scrollbar { display: none; } | |
| #player-hand { | |
| display: inline-flex; | |
| gap: 5px; | |
| padding: 0 5px; | |
| } | |
| .hand-card { | |
| transition: transform 0.1s; | |
| } | |
| .hand-card:active { | |
| transform: translateY(-10px); | |
| } | |
| /* UI Overlays */ | |
| #message-overlay { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(0,0,0,0.85); | |
| padding: 10px; | |
| border-radius: 10px; | |
| border: 1px solid var(--r1-orange); | |
| text-align: center; | |
| z-index: 100; | |
| display: none; | |
| width: 80%; | |
| } | |
| #color-picker { | |
| position: absolute; | |
| bottom: 110px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #222; | |
| padding: 8px; | |
| border-radius: 10px; | |
| display: none; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 5px; | |
| z-index: 110; | |
| } | |
| .color-btn { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 5px; | |
| border: none; | |
| } | |
| .btn { | |
| background: var(--r1-orange); | |
| color: white; | |
| border: none; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| font-size: 12px; | |
| margin-top: 5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div id="opponent-info"> | |
| <span>CPU</span> | |
| <span id="cpu-count">Cards: 7</span> | |
| </div> | |
| <div id="play-area"> | |
| <div id="draw-pile" class="card deck">?</div> | |
| <div id="discard-pile"></div> | |
| </div> | |
| <div id="message-overlay"> | |
| <div id="msg-text">Your Turn</div> | |
| <button class="btn" id="msg-btn">Next</button> | |
| </div> | |
| <div id="color-picker"> | |
| <button class="color-btn card-red" onclick="game.selectColor('red')"></button> | |
| <button class="color-btn card-blue" onclick="game.selectColor('blue')"></button> | |
| <button class="color-btn card-green" onclick="game.selectColor('green')"></button> | |
| <button class="color-btn card-yellow" onclick="game.selectColor('yellow')"></button> | |
| </div> | |
| <div id="player-hand-container"> | |
| <div id="player-hand"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const COLORS = ['red', 'blue', 'green', 'yellow']; | |
| const VALUES = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'S', 'R', '+2']; | |
| // Offline Sound Generator using Web Audio API | |
| class SoundEngine { | |
| constructor() { | |
| this.ctx = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| playTone(freq, type, duration, vol = 0.1) { | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = type; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime); | |
| gain.gain.setValueAtTime(vol, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration); | |
| osc.connect(gain); | |
| gain.connect(this.ctx.destination); | |
| osc.start(); | |
| osc.stop(this.ctx.currentTime + duration); | |
| } | |
| playCard() { this.playTone(440, 'sine', 0.1); } | |
| playDraw() { this.playTone(220, 'triangle', 0.15); } | |
| playPower() { | |
| this.playTone(660, 'square', 0.1, 0.05); | |
| setTimeout(() => this.playTone(880, 'square', 0.1, 0.05), 50); | |
| } | |
| playWin() { | |
| [440, 554, 659, 880].forEach((f, i) => { | |
| setTimeout(() => this.playTone(f, 'sine', 0.4, 0.1), i * 150); | |
| }); | |
| } | |
| playLose() { | |
| [440, 349, 293].forEach((f, i) => { | |
| setTimeout(() => this.playTone(f, 'sawtooth', 0.5, 0.05), i * 200); | |
| }); | |
| } | |
| } | |
| class UnoGame { | |
| constructor() { | |
| this.deck = []; | |
| this.playerHand = []; | |
| this.cpuHand = []; | |
| this.discardPile = []; | |
| this.turn = 'player'; | |
| this.isWildPending = false; | |
| this.sounds = new SoundEngine(); | |
| this.init(); | |
| } | |
| init() { | |
| this.createDeck(); | |
| this.shuffle(this.deck); | |
| for(let i=0; i<7; i++) { | |
| this.playerHand.push(this.deck.pop()); | |
| this.cpuHand.push(this.deck.pop()); | |
| } | |
| let firstCard = this.deck.pop(); | |
| while(firstCard.color === 'wild') { | |
| this.deck.unshift(firstCard); | |
| firstCard = this.deck.pop(); | |
| } | |
| this.discardPile.push(firstCard); | |
| this.render(); | |
| document.getElementById('draw-pile').onclick = () => { | |
| this.sounds.ctx.resume(); // Resume audio context on user interaction | |
| this.drawCard('player'); | |
| }; | |
| document.getElementById('msg-btn').onclick = () => { | |
| this.sounds.ctx.resume(); | |
| document.getElementById('message-overlay').style.display = 'none'; | |
| if(this.turn === 'cpu') this.cpuTurn(); | |
| }; | |
| } | |
| createDeck() { | |
| COLORS.forEach(color => { | |
| VALUES.forEach(val => { | |
| this.deck.push({ color, value: val }); | |
| if(val !== '0') this.deck.push({ color, value: val }); | |
| }); | |
| }); | |
| for(let i=0; i<4; i++) { | |
| this.deck.push({ color: 'wild', value: 'W' }); | |
| this.deck.push({ color: 'wild', value: '+4' }); | |
| } | |
| } | |
| shuffle(array) { | |
| for (let i = array.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [array[i], array[j]] = [array[j], array[i]]; | |
| } | |
| } | |
| render() { | |
| const handEl = document.getElementById('player-hand'); | |
| handEl.innerHTML = ''; | |
| this.playerHand.forEach((card, index) => { | |
| const div = document.createElement('div'); | |
| div.className = `card hand-card card-${card.color}`; | |
| div.innerText = card.value; | |
| div.onclick = () => { | |
| this.sounds.ctx.resume(); | |
| this.playCard(index); | |
| }; | |
| handEl.appendChild(div); | |
| }); | |
| const discard = this.discardPile[this.discardPile.length - 1]; | |
| const discardEl = document.getElementById('discard-pile'); | |
| discardEl.innerHTML = `<div class="card card-${discard.color}">${discard.value}</div>`; | |
| document.getElementById('cpu-count').innerText = `Cards: ${this.cpuHand.length}`; | |
| } | |
| showMessage(text) { | |
| const overlay = document.getElementById('message-overlay'); | |
| document.getElementById('msg-text').innerText = text; | |
| overlay.style.display = 'block'; | |
| if(text.includes("Win")) this.sounds.playWin(); | |
| if(text.includes("Lose") || text.includes("CPU Wins")) this.sounds.playLose(); | |
| } | |
| drawCard(who) { | |
| if(this.turn !== who || this.isWildPending) return; | |
| this.sounds.playDraw(); | |
| if(this.deck.length === 0) { | |
| const top = this.discardPile.pop(); | |
| this.deck = [...this.discardPile]; | |
| this.shuffle(this.deck); | |
| this.discardPile = [top]; | |
| } | |
| const card = this.deck.pop(); | |
| if(who === 'player') { | |
| this.playerHand.push(card); | |
| this.render(); | |
| if(!this.canPlay(card)) { | |
| this.turn = 'cpu'; | |
| setTimeout(() => this.showMessage("No playable card. CPU Turn"), 300); | |
| } | |
| } else { | |
| this.cpuHand.push(card); | |
| this.render(); | |
| this.turn = 'player'; | |
| } | |
| } | |
| canPlay(card) { | |
| const top = this.discardPile[this.discardPile.length - 1]; | |
| return card.color === 'wild' || card.color === top.color || card.value === top.value; | |
| } | |
| playCard(index) { | |
| if(this.turn !== 'player' || this.isWildPending) return; | |
| const card = this.playerHand[index]; | |
| if(this.canPlay(card)) { | |
| this.playerHand.splice(index, 1); | |
| this.processCard(card); | |
| } | |
| } | |
| processCard(card) { | |
| this.discardPile.push(card); | |
| // Sound logic | |
| if(card.color === 'wild' || card.value === '+2' || card.value === 'S' || card.value === 'R') { | |
| this.sounds.playPower(); | |
| } else { | |
| this.sounds.playCard(); | |
| } | |
| this.render(); | |
| if(this.playerHand.length === 0) return this.showMessage("You Win!"); | |
| if(this.cpuHand.length === 0) return this.showMessage("CPU Wins!"); | |
| if(card.color === 'wild') { | |
| this.isWildPending = true; | |
| document.getElementById('color-picker').style.display = 'grid'; | |
| if(card.value === '+4') { | |
| for(let i=0; i<4; i++) this.cpuHand.push(this.deck.pop()); | |
| } | |
| return; | |
| } | |
| let skip = false; | |
| if(card.value === 'S' || card.value === 'R') skip = true; | |
| if(card.value === '+2') { | |
| for(let i=0; i<2; i++) this.cpuHand.push(this.deck.pop()); | |
| skip = true; | |
| } | |
| if(skip) { | |
| this.render(); | |
| this.showMessage(`Effect! Player turns again`); | |
| } else { | |
| this.turn = 'cpu'; | |
| this.cpuTurn(); | |
| } | |
| } | |
| selectColor(color) { | |
| this.sounds.ctx.resume(); | |
| this.sounds.playCard(); | |
| const top = this.discardPile[this.discardPile.length - 1]; | |
| top.color = color; | |
| this.isWildPending = false; | |
| document.getElementById('color-picker').style.display = 'none'; | |
| this.turn = 'cpu'; | |
| this.render(); | |
| this.cpuTurn(); | |
| } | |
| cpuTurn() { | |
| setTimeout(() => { | |
| const playableIdx = this.cpuHand.findIndex(c => this.canPlay(c)); | |
| if(playableIdx > -1) { | |
| const card = this.cpuHand.splice(playableIdx, 1)[0]; | |
| if(card.color === 'wild') { | |
| card.color = COLORS[Math.floor(Math.random()*4)]; | |
| if(card.value === '+4') { | |
| for(let i=0; i<4; i++) this.playerHand.push(this.deck.pop()); | |
| } | |
| } | |
| this.discardPile.push(card); | |
| if(card.color === 'wild' || card.value === '+2' || card.value === 'S' || card.value === 'R') { | |
| this.sounds.playPower(); | |
| } else { | |
| this.sounds.playCard(); | |
| } | |
| this.render(); | |
| if(card.value === 'S' || card.value === 'R' || card.value === '+2') { | |
| if(card.value === '+2') for(let i=0; i<2; i++) this.playerHand.push(this.deck.pop()); | |
| this.render(); | |
| this.showMessage("CPU used power card! CPU turn again."); | |
| } else { | |
| this.turn = 'player'; | |
| this.render(); | |
| } | |
| } else { | |
| this.drawCard('cpu'); | |
| this.showMessage("CPU draws a card."); | |
| } | |
| }, 1000); | |
| } | |
| } | |
| const game = new UnoGame(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment