# 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();
});
```