# 311159023徐若曦_區塊鏈設計入門期末報告 ## Remix IDE ### Solidity #### conctracts/game v7.sol ##### 程式設計 ``` // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Game { uint8 private targetNumber; uint8 private targetSuit; uint256 private pool; address[] private players; mapping(address => uint8) private guessCountByAddress; mapping(address => Guess[]) private guessHistoryByAddress; address public owner; uint8 public reductionPercentage; struct Guess { uint8 number; uint8 suit; } event AllPlayers(address[] players); event AllPlayersCount(uint256 playersCount); event AllPlayersGuessCount(uint256 playersGuessCount); event DuplicateGuess(address indexed player, uint8 number, uint8 suit); event GameCorrect(address indexed player, uint8 number, uint8 suit, uint256 reward); event GuessIncorrect(address indexed player, uint8 number, uint8 suit, uint256 remainingReward); event RevealTarget(uint8 targetNumber, uint8 targetSuit); event ReductionPercentageUpdated(uint8 newReductionPercentage); modifier isOwner() { require(msg.sender == owner, "Caller is not owner"); _; } constructor(uint8 _targetNumber, uint8 _targetSuit, uint8 _initialReductionPercentage) { require(_targetNumber >= 1 && _targetNumber <= 13, "Number must be between 1 and 13"); require(_targetSuit >= 1 && _targetSuit <= 4, "Suit must be between 1 and 4"); require(_initialReductionPercentage <= 10, "Reduction percentage cannot exceed 10"); owner = msg.sender; targetNumber = _targetNumber; targetSuit = _targetSuit; reductionPercentage = _initialReductionPercentage; } function getAllPlayers() public isOwner { emit AllPlayers(players); } function getAllPlayersCount() public isOwner { emit AllPlayersCount(players.length); } function getAllPlayersGuessCount() public isOwner { uint256 totalGuessCount = 0; for (uint256 i = 0; i < players.length; i++) { totalGuessCount += guessCountByAddress[players[i]]; } emit AllPlayersGuessCount(totalGuessCount); } function getGuessCount() public view returns (uint8) { return guessCountByAddress[msg.sender]; } function getGuessHistory() public view returns (uint8[] memory, uint8[] memory){ Guess[] storage history = guessHistoryByAddress[msg.sender]; uint8[] memory numbers = new uint8[](history.length); uint8[] memory suits = new uint8[](history.length); for (uint256 i = 0; i < history.length; i++) { numbers[i] = history[i].number; suits[i] = history[i].suit; } return (numbers, suits); } function guess(uint8 _number, uint8 _suit) public payable { require(_number >= 1 && _number <= 13, "Number must be between 1 and 13"); require(_suit >= 1 && _suit <= 4, "Suit must be between 1 and 4"); require(msg.value == 1 ether, "You must send exactly 1 ETH"); Guess[] storage history = guessHistoryByAddress[msg.sender]; for (uint256 i = 0; i < history.length; i++) { if (history[i].number == _number && history[i].suit == _suit) { emit DuplicateGuess(msg.sender, _number, _suit); return; } } if (guessCountByAddress[msg.sender] == 0) { players.push(msg.sender); } pool += msg.value; guessCountByAddress[msg.sender]++; guessHistoryByAddress[msg.sender].push(Guess({number: _number, suit: _suit})); uint256 reward = calculateReward(); if (_number == targetNumber && _suit == targetSuit) { uint256 ownerShare = pool - reward; pool = 0; payable(msg.sender).transfer(reward); payable(owner).transfer(ownerShare); resetGame(); emit GameCorrect(msg.sender, targetNumber, targetSuit, reward); } else { emit GuessIncorrect(msg.sender, _number, _suit, reward); } } function revealTarget() public isOwner { emit RevealTarget(targetNumber, targetSuit); } function setNewTarget(uint8 _newTargetNumber, uint8 _newTargetSuit) public isOwner { require(_newTargetNumber >= 1 && _newTargetNumber <= 13, "Number must be between 1 and 13"); require(_newTargetSuit >= 1 && _newTargetSuit <= 4, "Suit must be between 1 and 4"); targetNumber = _newTargetNumber; targetSuit = _newTargetSuit; } function setReductionPercentage(uint8 _reductionPercentage) public isOwner { require(_reductionPercentage <= 10, "Reduction percentage cannot exceed 10"); reductionPercentage = _reductionPercentage; emit ReductionPercentageUpdated(_reductionPercentage); } function calculateReward() internal view returns (uint256) { uint256 totalReduction = reductionPercentage * guessCountByAddress[msg.sender]; if (totalReduction > 90) { totalReduction = 90; } return pool * (100 - totalReduction) / 100; } function resetGame() internal { for (uint256 i = 0; i < players.length; i++) { address player = players[i]; delete guessCountByAddress[player]; delete guessHistoryByAddress[player]; } delete players; } } ``` ### TypeScript #### scripts/deploy v7.ts ##### 程式設計 ``` import { ethers } from 'ethers'; import { deploy } from './ethers-lib'; async function main() { const initialTargetNumber = Math.floor(Math.random() * 13) + 1; const initialTargetSuit = Math.floor(Math.random() * 4) + 1; const initialReductionPercentage = 5; const gameContract = await deploy('Game', [ initialTargetNumber, initialTargetSuit, initialReductionPercentage, ]); console.log(`Game contract deployed at address: ${gameContract.address}`); console.log(`Initial reduction percentage set to: ${initialReductionPercentage}%`); gameContract.on("AllPlayers", (players) => { console.log("All players emitted:"); players.forEach((player: string, index: number) => { console.log(`Player ${index + 1}: ${player}`); }); }); gameContract.on("AllPlayersCount", (count) => { console.log(`Total Players Count: ${count}`); }); gameContract.on("AllPlayersGuessCount", (totalGuessCount) => { console.log(`Total Players Guess Count: ${totalGuessCount}`); }); gameContract.on("DuplicateGuess", (player, number, suit) => { console.log(`Duplicate guess detected from ${player}`); console.log(`Guess: Number ${number}, Suit ${suit}`); }); gameContract.on("GameCorrect", async (player, number, suit, reward) => { console.log(`Correct guess by ${player}`); console.log(`Guess: Number ${number}, Suit ${suit}`); console.log(`Reward: ${ethers.utils.formatEther(reward)} ETH`); const newTargetNumber = Math.floor(Math.random() * 13) + 1; const newTargetSuit = Math.floor(Math.random() * 4) + 1; await gameContract.setNewTarget(newTargetNumber, newTargetSuit); }); gameContract.on("GuessIncorrect", (player, number, suit, remainingReward) => { console.log(`Incorrect guess by ${player}`); console.log(`Guess: Number ${number}, Suit ${suit}`); console.log(`Remaining potential reward: ${ethers.utils.formatEther(remainingReward)} ETH`); }); gameContract.on("RevealTarget", (number, suit) => { console.log(`Target card revealed: Number ${number}, Suit ${suit}`); }); gameContract.on("ReductionPercentageUpdated", (newReductionPercentage) => { console.log(`Reduction percentage updated to: ${newReductionPercentage}%`); }); } main().catch((error) => { console.error("Error in script execution:", error.message); }); ``` ## Front-end ### HTML #### index v5.html ##### 程式設計 ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Blockchain Card Guessing Game</title> <link rel="stylesheet" href="style v5.css"> <script src="https://cdn.jsdelivr.net/npm/ethers@5.7.2/dist/ethers.umd.min.js"></script> <script src="script v5.js" defer></script> </head> <body> <div class="game-container"> <header> <h1>Blockchain Card Guessing Game</h1> <p>Guess the target card (number and suit) to win rewards!</p> </header> <section class="guess-form"> <h2>Make a Guess</h2> <label for="number">Card Number (1-13):</label> <input type="number" id="number" placeholder="Enter a number (1-13)" min="1" max="13"> <label for="suit">Card Suit (1-4):</label> <input type="number" id="suit" placeholder="Enter a suit (1-4)" min="1" max="4"> <button id="guessButton">Submit Guess (1 ETH)</button> </section> <section class="event-log"> <h2>Game Events</h2> <ul id="eventLog"></ul> </section> <section class="owner-actions"> <h2>Owner Controls</h2> <button id="revealButton">Reveal Target Card</button> </section> </div> </body> </html> ``` ### CSS #### style v5.css ##### 程式設計 ``` body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f4f8; color: #333; } .game-container { max-width: 800px; margin: 50px auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } header { text-align: center; margin-bottom: 20px; } header h1 { color: #4caf50; margin: 0; } header p { color: #666; margin: 10px 0 20px; } section { margin-bottom: 30px; } h2 { border-bottom: 2px solid #4caf50; padding-bottom: 5px; margin-bottom: 15px; color: #4caf50; } label { display: block; margin-top: 10px; font-weight: bold; } input { width: 100%; padding: 8px; margin: 5px 0 15px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { background-color: #4caf50; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } button:hover { background-color: #45a049; } .event-log ul { list-style: none; padding: 0; } .event-log li { margin: 10px 0; padding: 10px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; } ``` ### JavaScript #### script v5.js ##### 程式設計 ``` const contractAddress = "YOUR_CONTRACT_ADDRESS"; const contractABI = [ ]; document.addEventListener("DOMContentLoaded", () => { let provider, signer, gameContract; async function initializeContract() { if (!window.ethereum) { alert("MetaMask is required to use this application."); return; } provider = new ethers.providers.Web3Provider(window.ethereum); signer = provider.getSigner(); gameContract = new ethers.Contract(contractAddress, contractABI, signer); gameContract.on("GameCorrect", handleGameCorrect); gameContract.on("GuessIncorrect", handleGuessIncorrect); gameContract.on("DuplicateGuess", handleDuplicateGuess); gameContract.on("RevealTarget", handleRevealTarget); } function logEvent(message) { const eventLog = document.getElementById("eventLog"); const listItem = document.createElement("li"); listItem.textContent = message; eventLog.appendChild(listItem); } async function handleGameCorrect(player, number, suit, reward) { logEvent(`🎉 Correct guess by ${player}. Card: ${number}-${suit}. Reward: ${ethers.utils.formatEther(reward)} ETH`); } async function handleGuessIncorrect(player, number, suit, remainingReward) { logEvent(`❌ Incorrect guess by ${player}. Card: ${number}-${suit}. Remaining reward: ${ethers.utils.formatEther(remainingReward)} ETH`); } async function handleDuplicateGuess(player, number, suit) { logEvent(`⚠️ Duplicate guess by ${player}. Card: ${number}-${suit}`); } async function handleRevealTarget(number, suit) { logEvent(`🔍 Target card revealed: Number ${number}, Suit ${suit}`); } document.getElementById("guessButton").addEventListener("click", async () => { const number = parseInt(document.getElementById("number").value); const suit = parseInt(document.getElementById("suit").value); if (!number || !suit || number < 1 || number > 13 || suit < 1 || suit > 4) { alert("Please enter valid values for number (1-13) and suit (1-4)."); return; } try { const tx = await gameContract.guess(number, suit, { value: ethers.utils.parseEther("1") }); logEvent(`⏳ Guess submitted. Transaction hash: ${tx.hash}`); await tx.wait(); logEvent(`✅ Transaction confirmed.`); } catch (error) { logEvent(`⚠️ Error: ${error.message}`); } }); document.getElementById("revealButton").addEventListener("click", async () => { try { await gameContract.revealTarget(); logEvent(`🔍 Target card reveal requested.`); } catch (error) { logEvent(`⚠️ Error: ${error.message}`); } }); initializeContract(); }); ```