From ff960beed73c4e64944fc3b601bffeaea1f0bcc5 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Tue, 26 Aug 2025 18:55:27 -0700 Subject: [PATCH 1/3] Setup basic HTML5 project structure for Scrabble game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created index.html with proper HTML5 structure and viewport meta tag - Added CSS styling with responsive design and gradient background - Implemented JavaScript game initialization with console logging - Created basic game object structure with config and placeholder methods - Added README.md with project documentation - Organized files in clear directory structure (css/, js/) - All requirements met: centered title, welcome message, game container placeholder - Tested and verified cross-browser compatibility and mobile responsiveness 🤖 Generated with Claude Code Co-Authored-By: Claude --- README.md | 95 +++++++++++++++++++++++++++++++++++++ css/styles.css | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 30 ++++++++++++ js/game.js | 80 +++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 README.md create mode 100644 css/styles.css create mode 100644 index.html create mode 100644 js/game.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d97d44 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# Scrabble Game - HTML5 Interactive Multiplayer + +An interactive multiplayer HTML5 implementation of the classic Scrabble word game. + +## Project Structure + +``` +scrabble-game/ +├── index.html # Main game page +├── css/ +│ └── styles.css # Game styling and responsive design +├── js/ +│ └── game.js # Core game logic and initialization +└── README.md # Project documentation +``` + +## Features (Planned) + +- Interactive game board +- Multiplayer support (up to 4 players) +- Word validation +- Score calculation +- Responsive design for mobile and desktop +- Turn-based gameplay +- Tile management system + +## Getting Started + +1. Open `index.html` in a modern web browser +2. The game will initialize automatically +3. Check the browser console for initialization confirmation + +## Development Status + +### ✅ Completed +- Basic HTML5 project structure +- Responsive CSS styling +- JavaScript game object framework +- Mobile-friendly design + +### 🚧 In Progress +- Game board rendering +- Tile system implementation +- Player management + +### 📋 Planned +- Multiplayer functionality +- Dictionary integration +- Score tracking +- Game state persistence +- Sound effects +- Animations + +## Technical Stack + +- **HTML5** - Semantic markup and game structure +- **CSS3** - Styling with responsive design +- **Vanilla JavaScript (ES6)** - Game logic without external dependencies + +## Browser Compatibility + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) +- Mobile browsers (iOS Safari, Chrome Mobile) + +## Game Configuration + +The game configuration can be modified in `js/game.js`: + +```javascript +config: { + boardSize: 15, + maxPlayers: 4, + tilesPerPlayer: 7 +} +``` + +## Future Enhancements + +- Online multiplayer via WebSockets +- AI opponent +- Tournament mode +- Statistics tracking +- Custom word lists +- Theme customization + +## Contributing + +This project is part of the TEST-104 initiative to build an interactive multiplayer HTML5 game of Scrabble. + +## License + +This project is for educational purposes as part of the development workflow testing. \ No newline at end of file diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..5a3f04a --- /dev/null +++ b/css/styles.css @@ -0,0 +1,124 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.container { + background: white; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + padding: 40px; + max-width: 900px; + width: 100%; + animation: fadeIn 0.5s ease-in; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +.game-title { + font-size: 3rem; + color: #5a67d8; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); + font-weight: 700; + letter-spacing: 2px; +} + +.welcome-section { + text-align: center; + margin-bottom: 40px; +} + +.welcome-message { + font-size: 1.2rem; + color: #4a5568; + max-width: 600px; + margin: 0 auto; +} + +.game-container { + min-height: 400px; + background: #f7fafc; + border: 2px dashed #cbd5e0; + border-radius: 15px; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + transition: all 0.3s ease; +} + +.game-container:hover { + border-color: #9f7aea; + background: #faf5ff; +} + +.placeholder-text { + color: #a0aec0; + font-size: 1.1rem; + font-style: italic; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .game-title { + font-size: 2rem; + } + + .container { + padding: 30px 20px; + } + + .welcome-message { + font-size: 1rem; + } + + .game-container { + min-height: 300px; + } +} + +@media (max-width: 480px) { + .game-title { + font-size: 1.5rem; + letter-spacing: 1px; + } + + .container { + padding: 20px 15px; + border-radius: 10px; + } + + .welcome-message { + font-size: 0.9rem; + } + + .game-container { + min-height: 250px; + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..161bfd1 --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + + + Scrabble Game + + + +
+
+

Scrabble Game

+
+ +
+

+ Welcome to Scrabble! Get ready to challenge your vocabulary and strategic thinking skills. +

+
+ +
+ +

Game board will appear here

+
+
+ + + + \ No newline at end of file diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..f5315c5 --- /dev/null +++ b/js/game.js @@ -0,0 +1,80 @@ +'use strict'; + +const ScrabbleGame = { + config: { + boardSize: 15, + tilePoints: { + A: 1, B: 3, C: 3, D: 2, E: 1, F: 4, G: 2, H: 4, + I: 1, J: 8, K: 5, L: 1, M: 3, N: 1, O: 1, P: 3, + Q: 10, R: 1, S: 1, T: 1, U: 1, V: 4, W: 4, X: 8, + Y: 4, Z: 10 + }, + maxPlayers: 4, + tilesPerPlayer: 7 + }, + + state: { + isInitialized: false, + currentPlayer: null, + players: [], + board: [], + tileBag: [], + score: {} + }, + + init() { + console.log('Scrabble game initialized'); + + this.state.isInitialized = true; + + this.setupEventListeners(); + + this.displayInitMessage(); + }, + + setupEventListeners() { + const gameContainer = document.getElementById('game-container'); + if (gameContainer) { + gameContainer.addEventListener('click', () => { + console.log('Game container clicked - ready for future game board interaction'); + }); + } + }, + + displayInitMessage() { + const placeholderText = document.querySelector('.placeholder-text'); + if (placeholderText) { + placeholderText.textContent = 'Game initialized - Ready to play!'; + } + }, + + createBoard() { + console.log('Board creation method ready for implementation'); + }, + + startGame() { + console.log('Start game method ready for implementation'); + }, + + endTurn() { + console.log('End turn method ready for implementation'); + }, + + calculateScore(word) { + console.log('Score calculation method ready for implementation'); + return 0; + }, + + validateWord(word) { + console.log('Word validation method ready for implementation'); + return false; + } +}; + +document.addEventListener('DOMContentLoaded', () => { + console.log('DOM Content Loaded - Initializing Scrabble Game'); + + ScrabbleGame.init(); + + window.ScrabbleGame = ScrabbleGame; +}); \ No newline at end of file From 5e2be19390959c3298c1e3762991b9f4613b2033 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Tue, 26 Aug 2025 19:04:37 -0700 Subject: [PATCH 2/3] Implement 15x15 Scrabble game board with special squares - Add complete board generation logic with 225 squares - Implement all special square types (TWS, DWS, TLS, DLS, Center) - Position special squares according to official Scrabble layout - Add coordinate labels (A-O columns, 1-15 rows) - Style board with distinct colors for each square type - Implement responsive design for mobile/tablet/desktop - Add hover effects and click handlers with coordinate logging - Store board state in JavaScript data structure - Use CSS Grid for precise layout control - Add CSS custom properties for maintainable color scheme All requirements from TEST-106 successfully implemented. --- css/styles.css | 221 ++++++++++++++++++++++++++++++++++++++++++++++--- js/game.js | 176 +++++++++++++++++++++++++++++++++++---- 2 files changed, 371 insertions(+), 26 deletions(-) diff --git a/css/styles.css b/css/styles.css index 5a3f04a..09b3749 100644 --- a/css/styles.css +++ b/css/styles.css @@ -53,25 +53,153 @@ header { .game-container { min-height: 400px; - background: #f7fafc; - border: 2px dashed #cbd5e0; - border-radius: 15px; + background: transparent; display: flex; justify-content: center; align-items: center; padding: 20px; - transition: all 0.3s ease; } -.game-container:hover { - border-color: #9f7aea; - background: #faf5ff; +:root { + --square-regular: #F5E6D3; + --square-tw: #DC143C; + --square-dw: #FFB6C1; + --square-tl: #00008B; + --square-dl: #87CEEB; + --square-center: #FFD700; + --board-border: #8B4513; + --square-border: #D2B48C; } -.placeholder-text { - color: #a0aec0; - font-size: 1.1rem; - font-style: italic; +.board-wrapper { + display: flex; + flex-direction: column; + align-items: center; + background: white; + padding: 10px; + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.board-with-labels { + display: flex; + align-items: center; +} + +.board-grid { + display: grid; + grid-template-columns: repeat(15, 1fr); + grid-template-rows: repeat(15, 1fr); + gap: 1px; + background: var(--square-border); + border: 3px solid var(--board-border); + padding: 1px; + aspect-ratio: 1; + width: min(600px, calc(100vw - 120px)); + height: min(600px, calc(100vw - 120px)); +} + +.board-square { + background: var(--square-regular); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + aspect-ratio: 1; +} + +.board-square:hover { + transform: scale(1.05); + z-index: 10; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.board-square.triple-word { + background: var(--square-tw); +} + +.board-square.double-word { + background: var(--square-dw); +} + +.board-square.triple-letter { + background: var(--square-tl); +} + +.board-square.double-letter { + background: var(--square-dl); +} + +.board-square.center { + background: var(--square-center); +} + +.square-label { + font-size: 0.7em; + font-weight: bold; + text-align: center; + pointer-events: none; +} + +.board-square.triple-word .square-label, +.board-square.triple-letter .square-label { + color: white; +} + +.board-square.double-word .square-label { + color: #8B0000; +} + +.board-square.double-letter .square-label { + color: #00008B; +} + +.board-square.center .square-label { + font-size: 1.2em; + color: #8B4513; +} + +.coordinate-labels { + display: flex; + font-weight: bold; + color: #5a67d8; + font-size: 0.9rem; +} + +.coordinate-labels.top, +.coordinate-labels.bottom { + flex-direction: row; + width: 100%; + justify-content: center; + margin: 5px 0; +} + +.coordinate-labels.left, +.coordinate-labels.right { + flex-direction: column; + height: min(600px, calc(100vw - 120px)); + justify-content: space-around; + margin: 0 10px; +} + +.coordinate-label { + display: flex; + justify-content: center; + align-items: center; + width: calc(min(600px, calc(100vw - 120px)) / 15); + height: calc(min(600px, calc(100vw - 120px)) / 15); +} + +.coordinate-labels.left .coordinate-label, +.coordinate-labels.right .coordinate-label { + width: auto; + height: calc(min(600px, calc(100vw - 120px)) / 15); +} + +.label-spacer { + width: 13px; } @keyframes fadeIn { @@ -92,6 +220,7 @@ header { .container { padding: 30px 20px; + max-width: 100%; } .welcome-message { @@ -100,6 +229,32 @@ header { .game-container { min-height: 300px; + padding: 10px; + } + + .board-grid { + width: min(500px, calc(100vw - 80px)); + height: min(500px, calc(100vw - 80px)); + } + + .coordinate-labels.left, + .coordinate-labels.right { + height: min(500px, calc(100vw - 80px)); + } + + .coordinate-label { + width: calc(min(500px, calc(100vw - 80px)) / 15); + height: calc(min(500px, calc(100vw - 80px)) / 15); + font-size: 0.75rem; + } + + .coordinate-labels.left .coordinate-label, + .coordinate-labels.right .coordinate-label { + height: calc(min(500px, calc(100vw - 80px)) / 15); + } + + .square-label { + font-size: 0.6em; } } @@ -110,7 +265,7 @@ header { } .container { - padding: 20px 15px; + padding: 20px 10px; border-radius: 10px; } @@ -120,5 +275,47 @@ header { .game-container { min-height: 250px; + padding: 5px; + } + + .board-wrapper { + padding: 5px; + } + + .board-grid { + width: calc(100vw - 60px); + height: calc(100vw - 60px); + max-width: 400px; + max-height: 400px; + } + + .coordinate-labels.left, + .coordinate-labels.right { + height: calc(100vw - 60px); + max-height: 400px; + margin: 0 5px; + } + + .coordinate-label { + width: calc((100vw - 60px) / 15); + height: calc((100vw - 60px) / 15); + max-width: calc(400px / 15); + max-height: calc(400px / 15); + font-size: 0.65rem; + } + + .coordinate-labels.left .coordinate-label, + .coordinate-labels.right .coordinate-label { + width: auto; + height: calc((100vw - 60px) / 15); + max-height: calc(400px / 15); + } + + .square-label { + font-size: 0.5em; + } + + .board-square.center .square-label { + font-size: 0.9em; } } \ No newline at end of file diff --git a/js/game.js b/js/game.js index f5315c5..bedf892 100644 --- a/js/game.js +++ b/js/game.js @@ -10,7 +10,39 @@ const ScrabbleGame = { Y: 4, Z: 10 }, maxPlayers: 4, - tilesPerPlayer: 7 + tilesPerPlayer: 7, + specialSquares: { + tripleWord: [ + [0, 0], [0, 7], [0, 14], + [7, 0], [7, 14], + [14, 0], [14, 7], [14, 14] + ], + doubleWord: [ + [1, 1], [2, 2], [3, 3], [4, 4], + [1, 13], [2, 12], [3, 11], [4, 10], + [13, 1], [12, 2], [11, 3], [10, 4], + [13, 13], [12, 12], [11, 11], [10, 10], + [7, 7] + ], + tripleLetter: [ + [1, 5], [1, 9], + [5, 1], [5, 5], [5, 9], [5, 13], + [9, 1], [9, 5], [9, 9], [9, 13], + [13, 5], [13, 9] + ], + doubleLetter: [ + [0, 3], [0, 11], + [2, 6], [2, 8], + [3, 0], [3, 7], [3, 14], + [6, 2], [6, 6], [6, 8], [6, 12], + [7, 3], [7, 11], + [8, 2], [8, 6], [8, 8], [8, 12], + [11, 0], [11, 7], [11, 14], + [12, 6], [12, 8], + [14, 3], [14, 11] + ], + center: [[7, 7]] + } }, state: { @@ -26,30 +58,146 @@ const ScrabbleGame = { console.log('Scrabble game initialized'); this.state.isInitialized = true; - + this.createBoard(); this.setupEventListeners(); - - this.displayInitMessage(); }, setupEventListeners() { - const gameContainer = document.getElementById('game-container'); - if (gameContainer) { - gameContainer.addEventListener('click', () => { - console.log('Game container clicked - ready for future game board interaction'); + const squares = document.querySelectorAll('.board-square'); + squares.forEach(square => { + square.addEventListener('click', (e) => { + const row = parseInt(e.target.dataset.row); + const col = parseInt(e.target.dataset.col); + const coords = this.getCoordinateString(row, col); + console.log(`Square clicked: ${coords} (row: ${row}, col: ${col})`); }); - } + }); }, - displayInitMessage() { - const placeholderText = document.querySelector('.placeholder-text'); - if (placeholderText) { - placeholderText.textContent = 'Game initialized - Ready to play!'; + getCoordinateString(row, col) { + const colLetter = String.fromCharCode(65 + col); + const rowNumber = row + 1; + return `${colLetter}${rowNumber}`; + }, + + getSquareType(row, col) { + const { specialSquares } = this.config; + + if (row === 7 && col === 7) { + return 'center'; + } + + for (const [r, c] of specialSquares.tripleWord) { + if (r === row && c === col) return 'triple-word'; + } + + for (const [r, c] of specialSquares.doubleWord) { + if (r === row && c === col) return 'double-word'; + } + + for (const [r, c] of specialSquares.tripleLetter) { + if (r === row && c === col) return 'triple-letter'; } + + for (const [r, c] of specialSquares.doubleLetter) { + if (r === row && c === col) return 'double-letter'; + } + + return 'regular'; + }, + + getSquareLabel(type) { + const labels = { + 'triple-word': '3W', + 'double-word': '2W', + 'triple-letter': '3L', + 'double-letter': '2L', + 'center': '★', + 'regular': '' + }; + return labels[type] || ''; }, createBoard() { - console.log('Board creation method ready for implementation'); + const gameContainer = document.getElementById('game-container'); + if (!gameContainer) return; + + gameContainer.innerHTML = ''; + + const boardWrapper = document.createElement('div'); + boardWrapper.className = 'board-wrapper'; + + const boardGrid = document.createElement('div'); + boardGrid.className = 'board-grid'; + boardGrid.id = 'scrabble-board'; + + this.addCoordinateLabels(boardWrapper, 'top'); + + const boardWithSideLabels = document.createElement('div'); + boardWithSideLabels.className = 'board-with-labels'; + + this.addCoordinateLabels(boardWithSideLabels, 'left'); + boardWithSideLabels.appendChild(boardGrid); + this.addCoordinateLabels(boardWithSideLabels, 'right'); + + boardWrapper.appendChild(boardWithSideLabels); + this.addCoordinateLabels(boardWrapper, 'bottom'); + + this.state.board = []; + for (let row = 0; row < this.config.boardSize; row++) { + this.state.board[row] = []; + for (let col = 0; col < this.config.boardSize; col++) { + const square = document.createElement('div'); + square.className = 'board-square'; + square.dataset.row = row; + square.dataset.col = col; + + const squareType = this.getSquareType(row, col); + square.classList.add(squareType); + + const label = this.getSquareLabel(squareType); + if (label) { + const labelSpan = document.createElement('span'); + labelSpan.className = 'square-label'; + labelSpan.textContent = label; + square.appendChild(labelSpan); + } + + boardGrid.appendChild(square); + this.state.board[row][col] = { + tile: null, + type: squareType + }; + } + } + + gameContainer.appendChild(boardWrapper); + console.log('15x15 Scrabble board created successfully'); + }, + + addCoordinateLabels(container, position) { + const labelContainer = document.createElement('div'); + labelContainer.className = `coordinate-labels ${position}`; + + if (position === 'top' || position === 'bottom') { + labelContainer.innerHTML = '
'; + for (let i = 0; i < this.config.boardSize; i++) { + const label = document.createElement('div'); + label.className = 'coordinate-label'; + label.textContent = String.fromCharCode(65 + i); + labelContainer.appendChild(label); + } + labelContainer.innerHTML += '
'; + } else { + for (let i = 0; i < this.config.boardSize; i++) { + const label = document.createElement('div'); + label.className = 'coordinate-label'; + label.textContent = i + 1; + labelContainer.appendChild(label); + } + } + + container.appendChild(labelContainer); }, startGame() { From 6b9d345187ec84a1f2f4f5376a7341e0265bf394 Mon Sep 17 00:00:00 2001 From: Cyrus Date: Tue, 26 Aug 2025 19:16:46 -0700 Subject: [PATCH 3/3] Implement draggable letter tiles with point values for Scrabble game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add complete Scrabble letter distribution (100 tiles with correct counts) - Implement drag-and-drop functionality for desktop and mobile - Create tile rack interface holding 7 tiles with shuffle/draw buttons - Add comprehensive tile state management system - Style tiles with 3D appearance, point values, and visual feedback - Support drag between rack/board with snap-to-grid behavior - Enforce single tile per square validation - Add touch event support for mobile devices - Implement hover effects and drag state animations All 10 success criteria from TEST-107 have been met. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- css/styles.css | 203 +++++++++++++++++++++++++ index.html | 13 ++ js/game.js | 401 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 615 insertions(+), 2 deletions(-) diff --git a/css/styles.css b/css/styles.css index 09b3749..17161d8 100644 --- a/css/styles.css +++ b/css/styles.css @@ -258,6 +258,175 @@ header { } } +.tile-rack-container { + margin-top: 30px; + background: white; + border-radius: 15px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.tile-rack-wrapper { + text-align: center; +} + +.rack-title { + color: #5a67d8; + margin-bottom: 15px; + font-size: 1.2rem; +} + +.tile-rack { + display: flex; + justify-content: center; + gap: 10px; + min-height: 70px; + background: linear-gradient(135deg, #8B7355 0%, #6B5D54 100%); + border-radius: 10px; + padding: 15px; + box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2); + margin-bottom: 20px; +} + +.tile-slot { + width: 55px; + height: 55px; + background: rgba(255, 255, 255, 0.1); + border: 2px dashed rgba(255, 255, 255, 0.3); + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; +} + +.tile { + width: 50px; + height: 50px; + background: #FFF8DC; + border: 2px solid #D2B48C; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + cursor: grab; + font-weight: bold; + font-size: 24px; + color: #2C3E50; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2), + 0 1px 2px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + user-select: none; +} + +.tile:hover { + transform: translateY(-2px); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3), + 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.tile.dragging { + opacity: 0.5; + cursor: grabbing; + transform: scale(1.1); + z-index: 1000; +} + +.tile.dragging-active { + opacity: 0.7; + cursor: grabbing; + transform: scale(1.05); +} + +.tile-letter { + font-size: 24px; + font-weight: bold; + text-align: center; +} + +.tile-points { + position: absolute; + bottom: 2px; + right: 4px; + font-size: 10px; + font-weight: bold; + color: #5C4033; +} + +.tile.blank .tile-letter { + color: transparent; +} + +.tile.blank .tile-points { + display: none; +} + +.board-square.occupied { + background-color: #E8DCC0; +} + +.board-square.valid-drop { + background-color: #90EE90 !important; + animation: pulse 0.5s ease-in-out infinite; +} + +.board-square.invalid-drop { + background-color: #FFB6C1 !important; +} + +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +.game-controls { + display: flex; + justify-content: center; + gap: 15px; +} + +.game-btn { + padding: 10px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.game-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.game-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.game-btn:disabled { + background: #CBD5E0; + cursor: not-allowed; + transform: none; +} + +.board-square .tile { + width: 90%; + height: 90%; + margin: auto; +} + @media (max-width: 480px) { .game-title { font-size: 1.5rem; @@ -318,4 +487,38 @@ header { .board-square.center .square-label { font-size: 0.9em; } + + .tile-rack { + gap: 5px; + padding: 10px; + } + + .tile-slot { + width: 40px; + height: 40px; + } + + .tile { + width: 36px; + height: 36px; + font-size: 18px; + } + + .tile-letter { + font-size: 18px; + } + + .tile-points { + font-size: 8px; + } + + .game-controls { + flex-direction: column; + gap: 10px; + } + + .game-btn { + font-size: 0.9rem; + padding: 8px 16px; + } } \ No newline at end of file diff --git a/index.html b/index.html index 161bfd1..e362a63 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,19 @@

Scrabble Game

Game board will appear here

+ +
+
+

Your Tiles

+
+ +
+
+ + +
+
+
diff --git a/js/game.js b/js/game.js index bedf892..e1d8a6f 100644 --- a/js/game.js +++ b/js/game.js @@ -7,7 +7,13 @@ const ScrabbleGame = { A: 1, B: 3, C: 3, D: 2, E: 1, F: 4, G: 2, H: 4, I: 1, J: 8, K: 5, L: 1, M: 3, N: 1, O: 1, P: 3, Q: 10, R: 1, S: 1, T: 1, U: 1, V: 4, W: 4, X: 8, - Y: 4, Z: 10 + Y: 4, Z: 10, BLANK: 0 + }, + tileDistribution: { + A: 9, B: 2, C: 2, D: 4, E: 12, F: 2, G: 3, H: 2, + I: 9, J: 1, K: 1, L: 4, M: 2, N: 6, O: 8, P: 2, + Q: 1, R: 6, S: 4, T: 6, U: 4, V: 2, W: 2, X: 1, + Y: 2, Z: 1, BLANK: 2 }, maxPlayers: 4, tilesPerPlayer: 7, @@ -51,7 +57,11 @@ const ScrabbleGame = { players: [], board: [], tileBag: [], - score: {} + playerRack: [], + tiles: {}, + score: {}, + draggedTile: null, + tileIdCounter: 0 }, init() { @@ -59,7 +69,348 @@ const ScrabbleGame = { this.state.isInitialized = true; this.createBoard(); + this.initializeTileBag(); + this.createTileRack(); this.setupEventListeners(); + this.drawInitialTiles(); + }, + + initializeTileBag() { + this.state.tileBag = []; + this.state.tiles = {}; + + for (const [letter, count] of Object.entries(this.config.tileDistribution)) { + for (let i = 0; i < count; i++) { + const tileId = `tile-${this.state.tileIdCounter++}`; + const tile = { + id: tileId, + letter: letter, + points: this.config.tilePoints[letter], + location: 'bag' + }; + this.state.tileBag.push(tile); + this.state.tiles[tileId] = tile; + } + } + + this.shuffleTileBag(); + console.log(`Tile bag initialized with ${this.state.tileBag.length} tiles`); + }, + + shuffleTileBag() { + for (let i = this.state.tileBag.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [this.state.tileBag[i], this.state.tileBag[j]] = + [this.state.tileBag[j], this.state.tileBag[i]]; + } + }, + + createTileRack() { + const rackElement = document.getElementById('tile-rack'); + if (!rackElement) return; + + rackElement.innerHTML = ''; + for (let i = 0; i < this.config.tilesPerPlayer; i++) { + const slot = document.createElement('div'); + slot.className = 'tile-slot'; + slot.dataset.slotIndex = i; + rackElement.appendChild(slot); + } + + this.state.playerRack = new Array(this.config.tilesPerPlayer).fill(null); + }, + + drawInitialTiles() { + const tilesToDraw = Math.min(this.config.tilesPerPlayer, this.state.tileBag.length); + for (let i = 0; i < tilesToDraw; i++) { + this.drawTileToRack(i); + } + }, + + drawTileToRack(slotIndex) { + if (this.state.tileBag.length === 0) { + console.log('No more tiles in bag'); + return null; + } + + if (this.state.playerRack[slotIndex] !== null) { + return null; + } + + const tile = this.state.tileBag.pop(); + tile.location = 'rack'; + tile.rackIndex = slotIndex; + this.state.playerRack[slotIndex] = tile; + + const tileElement = this.createTileElement(tile); + const slot = document.querySelector(`.tile-slot[data-slot-index="${slotIndex}"]`); + if (slot) { + slot.appendChild(tileElement); + } + + return tile; + }, + + createTileElement(tile) { + const tileDiv = document.createElement('div'); + tileDiv.className = 'tile'; + tileDiv.id = tile.id; + tileDiv.draggable = true; + + if (tile.letter === 'BLANK') { + tileDiv.classList.add('blank'); + } + + const letterSpan = document.createElement('span'); + letterSpan.className = 'tile-letter'; + letterSpan.textContent = tile.letter === 'BLANK' ? '' : tile.letter; + + const pointsSpan = document.createElement('span'); + pointsSpan.className = 'tile-points'; + pointsSpan.textContent = tile.points; + + tileDiv.appendChild(letterSpan); + tileDiv.appendChild(pointsSpan); + + this.setupTileDragEvents(tileDiv, tile); + + return tileDiv; + }, + + setupTileDragEvents(tileElement, tile) { + tileElement.addEventListener('dragstart', (e) => { + this.handleDragStart(e, tile); + }); + + tileElement.addEventListener('dragend', (e) => { + this.handleDragEnd(e, tile); + }); + + tileElement.addEventListener('touchstart', (e) => { + this.handleTouchStart(e, tile); + }, { passive: false }); + + tileElement.addEventListener('touchmove', (e) => { + this.handleTouchMove(e, tile); + }, { passive: false }); + + tileElement.addEventListener('touchend', (e) => { + this.handleTouchEnd(e, tile); + }, { passive: false }); + }, + + handleDragStart(e, tile) { + this.state.draggedTile = tile; + e.target.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', tile.id); + + console.log(`Started dragging tile: ${tile.letter} (${tile.points} points)`); + }, + + handleDragEnd(e, tile) { + e.target.classList.remove('dragging'); + this.state.draggedTile = null; + + document.querySelectorAll('.valid-drop, .invalid-drop').forEach(el => { + el.classList.remove('valid-drop', 'invalid-drop'); + }); + }, + + handleTouchStart(e, tile) { + e.preventDefault(); + this.state.draggedTile = tile; + const touch = e.touches[0]; + + const tileElement = e.target.closest('.tile'); + tileElement.classList.add('dragging-active'); + + tileElement.style.position = 'fixed'; + tileElement.style.zIndex = '1000'; + tileElement.style.left = `${touch.clientX - 25}px`; + tileElement.style.top = `${touch.clientY - 25}px`; + }, + + handleTouchMove(e, tile) { + e.preventDefault(); + const touch = e.touches[0]; + const tileElement = e.target.closest('.tile'); + + if (tileElement) { + tileElement.style.left = `${touch.clientX - 25}px`; + tileElement.style.top = `${touch.clientY - 25}px`; + + const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY); + if (elementBelow && elementBelow.classList.contains('board-square')) { + this.highlightDropTarget(elementBelow); + } + } + }, + + handleTouchEnd(e, tile) { + e.preventDefault(); + const tileElement = e.target.closest('.tile'); + const touch = e.changedTouches[0]; + + tileElement.classList.remove('dragging-active'); + tileElement.style.position = ''; + tileElement.style.zIndex = ''; + tileElement.style.left = ''; + tileElement.style.top = ''; + + const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY); + + if (elementBelow) { + if (elementBelow.classList.contains('board-square')) { + this.handleTileDrop(elementBelow, tile); + } else if (elementBelow.classList.contains('tile-slot')) { + this.returnTileToRack(tile, elementBelow); + } + } + + this.state.draggedTile = null; + document.querySelectorAll('.valid-drop, .invalid-drop').forEach(el => { + el.classList.remove('valid-drop', 'invalid-drop'); + }); + }, + + highlightDropTarget(square) { + document.querySelectorAll('.valid-drop, .invalid-drop').forEach(el => { + el.classList.remove('valid-drop', 'invalid-drop'); + }); + + const row = parseInt(square.dataset.row); + const col = parseInt(square.dataset.col); + + if (this.canPlaceTile(row, col)) { + square.classList.add('valid-drop'); + } else { + square.classList.add('invalid-drop'); + } + }, + + canPlaceTile(row, col) { + return this.state.board[row][col].tile === null; + }, + + placeTileOnBoard(tile, row, col) { + if (!this.canPlaceTile(row, col)) { + return false; + } + + if (tile.location === 'rack') { + this.state.playerRack[tile.rackIndex] = null; + delete tile.rackIndex; + } else if (tile.location === 'board') { + this.state.board[tile.boardRow][tile.boardCol].tile = null; + } + + this.state.board[row][col].tile = tile; + tile.location = 'board'; + tile.boardRow = row; + tile.boardCol = col; + + const square = document.querySelector(`.board-square[data-row="${row}"][data-col="${col}"]`); + const tileElement = document.getElementById(tile.id); + + if (square && tileElement) { + square.appendChild(tileElement); + square.classList.add('occupied'); + } + + console.log(`Placed ${tile.letter} at ${this.getCoordinateString(row, col)}`); + return true; + }, + + returnTileToRack(tile, slot) { + const slotIndex = parseInt(slot.dataset.slotIndex); + + if (this.state.playerRack[slotIndex] !== null) { + return false; + } + + if (tile.location === 'board') { + const square = document.querySelector( + `.board-square[data-row="${tile.boardRow}"][data-col="${tile.boardCol}"]` + ); + if (square) { + square.classList.remove('occupied'); + } + this.state.board[tile.boardRow][tile.boardCol].tile = null; + delete tile.boardRow; + delete tile.boardCol; + } else if (tile.location === 'rack' && tile.rackIndex !== slotIndex) { + this.state.playerRack[tile.rackIndex] = null; + } + + tile.location = 'rack'; + tile.rackIndex = slotIndex; + this.state.playerRack[slotIndex] = tile; + + const tileElement = document.getElementById(tile.id); + if (slot && tileElement) { + slot.appendChild(tileElement); + } + + return true; + }, + + handleTileDrop(square, tile) { + const row = parseInt(square.dataset.row); + const col = parseInt(square.dataset.col); + + this.placeTileOnBoard(tile, row, col); + }, + + getTileAtPosition(row, col) { + if (row < 0 || row >= this.config.boardSize || + col < 0 || col >= this.config.boardSize) { + return null; + } + return this.state.board[row][col].tile; + }, + + shuffleRackTiles() { + const rackTiles = this.state.playerRack.filter(tile => tile !== null); + const emptySlots = []; + + for (let i = 0; i < this.config.tilesPerPlayer; i++) { + if (this.state.playerRack[i] === null) { + emptySlots.push(i); + } + this.state.playerRack[i] = null; + } + + for (let i = rackTiles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [rackTiles[i], rackTiles[j]] = [rackTiles[j], rackTiles[i]]; + } + + const availableSlots = [...Array(this.config.tilesPerPlayer).keys()]; + rackTiles.forEach((tile, index) => { + const slotIndex = availableSlots[index]; + this.state.playerRack[slotIndex] = tile; + tile.rackIndex = slotIndex; + + const tileElement = document.getElementById(tile.id); + const slot = document.querySelector(`.tile-slot[data-slot-index="${slotIndex}"]`); + if (tileElement && slot) { + slot.appendChild(tileElement); + } + }); + + console.log('Rack tiles shuffled'); + }, + + refillRack() { + let tilesDrawn = 0; + for (let i = 0; i < this.config.tilesPerPlayer; i++) { + if (this.state.playerRack[i] === null && this.state.tileBag.length > 0) { + this.drawTileToRack(i); + tilesDrawn++; + } + } + console.log(`Drew ${tilesDrawn} new tiles. ${this.state.tileBag.length} tiles remaining in bag.`); }, setupEventListeners() { @@ -71,7 +422,53 @@ const ScrabbleGame = { const coords = this.getCoordinateString(row, col); console.log(`Square clicked: ${coords} (row: ${row}, col: ${col})`); }); + + square.addEventListener('dragover', (e) => { + e.preventDefault(); + if (this.state.draggedTile) { + this.highlightDropTarget(square); + } + }); + + square.addEventListener('drop', (e) => { + e.preventDefault(); + if (this.state.draggedTile) { + this.handleTileDrop(square, this.state.draggedTile); + } + }); + + square.addEventListener('dragleave', (e) => { + square.classList.remove('valid-drop', 'invalid-drop'); + }); }); + + const rackSlots = document.querySelectorAll('.tile-slot'); + rackSlots.forEach(slot => { + slot.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + slot.addEventListener('drop', (e) => { + e.preventDefault(); + if (this.state.draggedTile) { + this.returnTileToRack(this.state.draggedTile, slot); + } + }); + }); + + const drawBtn = document.getElementById('draw-tiles-btn'); + if (drawBtn) { + drawBtn.addEventListener('click', () => { + this.refillRack(); + }); + } + + const shuffleBtn = document.getElementById('shuffle-tiles-btn'); + if (shuffleBtn) { + shuffleBtn.addEventListener('click', () => { + this.shuffleRackTiles(); + }); + } }, getCoordinateString(row, col) {