当前位置: 首页 > news >正文

Next.js + Move 石头剪刀布

rock-paper-scissors

写在前面

本地

源码:https://github.com/zcy1024/SuiStudy/tree/main/rock-paper-scissors

# 或其它等价的命令来安装依赖并将项目跑起来
pnpm install
pnpm run dev
# http://localhost:3000/

在线(如果没过期的话)

https://rock-paper-scissors.walrus.site/

前端(样式布局)

初始化

用 Sui dApp 项目生成器创建一个使用 Sui Testnet 的 Next.js 框架,一切按照提示进行,生成器指南中有详细操作流程,这里就不再赘述。

config/index.ts是根据你所选择的网络环境进行初始化配置的文件,我们这里不需要动它。
contracts文件夹用来存储智能合约,lib/contracts文件夹用来存储前端对链上合约的调用,public文件夹用来存储静态文件,utils文件夹用来存储通用的函数或工具,清空这些文件夹里生成的样板代码(直接删除ts文件)。
app文件夹是网页的主体,我们也来对其做一些样板代码清理工作。首先,删除favicon.ico,这是网页标签页的图标,然后,进入page.tsx,将里面的代码除了基础结构外全部清除,就像这样:

'use client'import Image from 'next/image'export default function Home() {return (<div className=""><Image src="/logo/logo.jpg" alt="Sui Logo" width={80} height={40} /></div>);
}

准备好HOH社区的logo,将其替换掉/logo/logo.jpg,接下来更改app/layout.tsx文件中的metadata,该结构中的参数会影响网页标签页的展示内容:

export const metadata: Metadata = {title: "Rock Paper Scissors",description: "Classic Game: Rock Paper Scissors",icons: "/logo/logo.jpeg"
};

在 Next.js 中,使用public文件夹中的静态文件的时候,直接用/来表示public/,上面的<Image src="/logo/logo.jpg"... />也是同理。

最后,简单了解一下app下的其它内容:fonts/fonts.tsglobals.css是创建 Next.js 框架时自带的字体样式(处理)和全局css样式(已配置好tailwindcss),providers.tsx用来初始化 Sui 网络环境、钱包等配置。

至此,我们运行项目,应该能得到如下界面:

在这里插入图片描述

页面搭建

整体布局
  • 页面上方一条导航栏,左侧放logo,右侧放连接钱包的按钮。
  • 剩余部分都用来作为石头剪刀布的游戏区域。

得益于 Sui dApp 项目生成器的配置,连接钱包的按钮就只需要调用@mysten/dapp-kit中提供的组件ConnectButton

为了让布局更直观,我们暂时为上下两块区域加上背景颜色:

'use client'import Image from 'next/image'
import {ConnectButton} from "@mysten/dapp-kit";export default function Home() {return (<div className="flex flex-col h-screen mx-64"><div className="bg-red-600 flex justify-between items-center"><Image src="/logo/logo.jpeg" alt="HOH Logo" width={80} height={80} priority={true} /><ConnectButton /></div><div className="flex-1 bg-yellow-600"></div></div>);
}

在这里插入图片描述

游戏区域布局

寻找石头、剪刀、布的图片,存储至public/game/目录下,分别以rock.pngscissors.pngpaper.png命名,以其中任意一张作为样本,将其放到游戏区域的中央,这将作为游戏开始的点击按键,同时为其绑定触发函数:

const playGame = () => {console.log('play game');
}<div className="relative flex-1 bg-yellow-600"><div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 cursor-pointer" onClick={playGame}><Image src="/game/rock.png" alt="start button" width={100} height={100} priority={true} className="w-auto h-auto" /></div>
</div>

在这里插入图片描述

在这个开始按键的上方,是敌方(链上随机)选择区域;下方则是我方(鼠标点击)选择区域。类似的,用flex规划好区域后往里面填充内容:

<div className="relative flex-1 bg-yellow-600"><div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 cursor-pointer" onClick={playGame}><Image src="/game/rock.png" alt="start button" width={100} height={100} priority={true} className="w-auto h-auto" /></div><div className="absolute top-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="enemy" width={100} height={100} priority={true} className="w-auto h-auto" /></div><div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true} className="w-auto h-auto" /><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true} className="w-auto h-auto" /><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true} className="w-auto h-auto" /></div>
</div>

在这里插入图片描述

这时我们发现,中间的开始按钮失去了作用,仔细观察不难发现,这是因为后续的敌我双方的布局覆盖在了上方,最简单的办法是将开始按钮的代码向下移或者为其自定义z-index属性。

之后,我们来思考一个逻辑,开始按钮和后续的出拳选择是否真的需要同时出现?

我们完全可以先将出拳选择区域隐藏,在点击开始后再让其显现出来,相对应的,开始按钮则需要在点击后隐藏。不难发现,它们的显隐状态归根结底都由一个数据进行控制 —— 是否开始游戏。

const [isPlaying, setIsPlaying] = useState<boolean>(false);

用一个布尔值isPlaying来判断,点击后通过setIsPlaying将其设为真。
对于需要根据该值隐藏的内容,通过className={"..." + (isPlaying ? "..." : "...")}来设置。
为了消失和显现不那么突然,可以增加transition-opacity来实现渐隐渐显效果。

export default function Home() {const [isPlaying, setIsPlaying] = useState<boolean>(false);const playGame = () => {setIsPlaying(true);}return (<div className="flex flex-col h-screen mx-64"><div className="bg-red-600 flex justify-between items-center"><Image src="/logo/logo.jpeg" alt="HOH Logo" width={80} height={80} priority={true}/><ConnectButton/></div><div className="relative flex-1 bg-yellow-600"><divclassName={"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 transition-opacity " + (isPlaying ? "opacity-0" : "cursor-pointer opacity-100")}onClick={!isPlaying ? playGame : () => {}}><Image src="/game/rock.png" alt="start button" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className={"transition-opacity " + (isPlaying ? "opacity-100" : "opacity-0")}><div className="absolute top-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="enemy" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true}className="w-auto h-auto"/><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true}className="w-auto h-auto"/><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true}className="w-auto h-auto"/></div></div></div></div>);
}

在这里插入图片描述

类似的,我们为我方选择区域的三个图添加点击事件,由于它们都是<Image ... />,可以通过同一个类型的点击事件进行获取,最后通过alt属性来区分究竟选择的是石头、剪刀还是布。

const clickChoose = (e: MouseEvent<HTMLImageElement>) => {console.log(e.currentTarget.alt);
}<div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/>
</div>

在这里插入图片描述

游戏区域的布局是完成了,我们可以把之前用来辨别区域的背景去掉,纯白色太刺眼,就再加一点点灰色缓冲,但是,为什么开始按钮是石头?不如让它动起来,循环切换石头、剪刀、布,包括敌人(链上随机)在返回结果时也不应该固定显示。

大致思路:将三张图片的文件名放到一个数组中,通过不断加一再对数组长度取余使得下标达成循环,根据当前下标所对应的文件名进行显示渲染。

utils文件夹下创建三个文件sleep.tsnext.tsindex.ts

sleep.ts:顾名思义,让程序睡眠,等待多少时间后再继续向下运行。

export default function sleep(ms: number) {return new Promise(resolve => setTimeout(resolve, ms));
}

next.ts:在一个数组中循环不断地取下一个。

// 这里显示标注返回值中依次的类型,方便解构赋值后按照次序获得确切的类型
export default function next<T>(index: number, array: T[]): [number, T] {const len = array.length;index = (index + 1) % len;return [index, array[index]];
}

index.ts:将utils目录下所有导出的东西归档再一同导出,方便其它地方导入。由于这里只有两个函数,便捷性提升得不明显。

import sleep from './sleep';
import next from './next';export {sleep,next
}

回到page.tsx,借助上面的两个小函数来实现每隔一小段时间切换图片的功能:

  1. 图片名更新要实时作用到页面中,所以需要useState来创建一个字符串以及改变该字符串的函数:
    const [loopName, setLoopName] = useState<string>("rock");
  2. 定义一个下标,表示当前循环到了数组中的哪一项,很自然地想到用let index = 0;不过,用let定义的变量,除非放到全局,否则每次渲染都会重置。
    类似于useState,有一个钩子函数useRef可以解决这个问题:const index = useRef<number>(0);
    当需要取值时,用index.current,需要更改值时,也只需要将新值赋值给index.current
  3. 定义一个包含三张图片名的数组:const array = ["rock", "scissors", "paper"];
  4. 实现一个异步函数,在里面依次调用上面两个小工具,获得数据进行更新,而这个函数则放到useEffect当中,这个useEffect的依赖项设置为loopName,即每次loopName改变后重新执行。
const waitToDispatch = async () => {await sleep(222);const [ne_idx, name] = next(index.current, array);index.current = ne_idx;setLoopName(name);
}
useEffect(() => {waitToDispatch().then();
}, [loopName]);// 最后,将写死的<Image src="/game/rock.png" ... />改为<Image src={`/game/${loopName}.png`} ... />
// 每次loopName变化,src也会跟着变化

至此,功能已经实现且能够正常运行,不过,如果尝试build会发现其中还有一些警告,接下去来尝试解决一下:

useEffect`中用到了`waitToDispatch`,提示我们最好将其添加为依赖项,于是:`useEffect(..., [loopName, waitToDispatch])

再次build获得一个新的警告,由于waitToDispatchuseEffect的依赖项,所以它当前定义实现的位置,可能会因为重新渲染等因素出现潜在的问题。提示了两个解决方案,一个是转移实现waitToDispatch的位置,另一个是用useCallback包裹它。
useCallback实现的函数,它不会因为页面重新渲染而改变,除非它检测到它的依赖项发生变化才会更新其中的逻辑,起到缓存、提升性能的作用。
于是,我们用其包裹:

const waitToDispatch = useCallback(async () => {await sleep(222);const [ne_idx, name] = next(index.current, array);index.current = ne_idx;setLoopName(name);
}, [index, array]);

我们知道,array内部的值其实是不会改变的,所以只需要依赖index变化来变化就可以,实际上项目也可以运行,不过又会在build时警告,所以我们将其加上。不过,加上之后,又报了个新的warning,说是由于arrayuseCallback的依赖项,当前位置可能会出现潜在的问题,需要我们转移array定义的位置,或者用useMemo将其包裹。
useMemouseCallback类似,但是,useMemo得到的是经过逻辑运算后的值,并将这个值缓存下来,以避免重复进行(大量的)逻辑运算,除非它的依赖项的值发生了变化才会重新进行计算。
于是,我们用其包裹:const array = useMemo(() => ["rock", "scissors", "paper"], []);

终于,我们解决了所有警告!附上当下page.tsx的完整代码以及演示动图:

'use client'import Image from 'next/image'
import {ConnectButton} from "@mysten/dapp-kit";
import {MouseEvent, useCallback, useEffect, useMemo, useRef, useState} from "react";
import {sleep, next} from "@/utils"export default function Home() {const [isPlaying, setIsPlaying] = useState<boolean>(false);const playGame = () => {setIsPlaying(true);}const clickChoose = (e: MouseEvent<HTMLImageElement>) => {console.log(e.currentTarget.alt);}const [loopName, setLoopName] = useState<string>("rock");const index = useRef<number>(0);const array = useMemo(() => ["rock", "scissors", "paper"], []);const waitToDispatch = useCallback(async () => {await sleep(222);const [ne_idx, name] = next(index.current, array);index.current = ne_idx;setLoopName(name);}, [index, array]);useEffect(() => {waitToDispatch().then();}, [loopName, waitToDispatch]);return (<div className="flex flex-col h-screen mx-64 bg-gray-50 shadow-md"><div className="flex justify-between items-center"><Image src="/logo/logo.jpeg" alt="HOH Logo" width={80} height={80} priority={true}/><ConnectButton/></div><div className="relative flex-1"><divclassName={"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 transition-opacity " + (isPlaying ? "opacity-0" : "cursor-pointer opacity-100")}onClick={!isPlaying ? playGame : () => {}}><Image src={`/game/${loopName}.png`} alt="start button" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className={"transition-opacity " + (isPlaying ? "opacity-100" : "opacity-0")}><div className="absolute top-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src={`/game/${loopName}.png`} alt="enemy" width={100} height={100} priority={true}className="w-auto h-auto"/></div><div className="absolute bottom-0 left-0 w-full h-1/2 flex justify-evenly items-center"><Image src="/game/rock.png" alt="rock" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/scissors.png" alt="scissors" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/><Image src="/game/paper.png" alt="paper" width={100} height={100} priority={true}className={"w-auto h-auto " + (isPlaying ? "cursor-pointer" : "")}onClick={isPlaying ? clickChoose : () => {}}/></div></div></div></div>);
}

在这里插入图片描述

智能合约

前端页面布局暂告一段落,从这里开始将用Move编写一个简单的石头剪刀布的智能合约。

来到contracts目录,通过sui move new game命令新建合约代码。删除tests目录,里面用来编写测试代码,我们暂时用不上。打开sources/game.move准备编写合约。

我们想要达成的效果很简单,就是当玩家选择好自己是石头、剪刀还是布之后,通过链上随机的方式得到对方出什么。

于是,我们就需要编写以下内容:

  • 通过触发事件的方式来得到随机结果。
  • 随机函数。

最终,move代码如下:

module game::game {use sui::event;use sui::random::Random;public struct RandomEvent has copy, drop {chosen: u8}entry fun play(random: &Random, ctx: &mut TxContext) {let mut generator = random.new_generator(ctx);event::emit(RandomEvent {chosen: generator.generate_u8_in_range(1, 3)});}
}

随机得到1~3中的数,由前端处理其对应到石头、剪刀和布,sui move build没问题,sui client publish发布,成功后得到一串信息。

通过命令行调用初步观察结果:

export PACKAGE=0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b
sui client call --package $PACKAGE --module game --function play --args 0x8
# output:
╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Transaction Block Events                                                                            │
├─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│  ┌──                                                                                                │
│  │ EventID: o82egWHVDnqSABWre6tustng5zb6vDBfyQvPBtDGnQs:0                                           │
│  │ PackageID: 0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b                    │
│  │ Transaction Module: game                                                                         │
│  │ Sender: 0x9e4092b6a894e6b168aa1c6c009f5c1c1fcb83fb95e5aa39144e1d2be4ee0d67                       │
│  │ EventType: 0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b::game::RandomEvent │
│  │ ParsedJSON:                                                                                      │
│  │   ┌────────┬───┐                                                                                 │
│  │   │ chosen │ 3 │                                                                                 │
│  │   └────────┴───┘                                                                                 │
│  └──                                                                                                │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯

重复调用几次,发现触发的事件中的值确实是随机的,接着,我们对一些信息进行存储,打开config目录新建key.ts,用来存储发布后的ID,为了方便后续前端调用,我们可以把Random的地址、调用的完整函数名以及触发的EventType也存上并导出。

// UPGRADE_CAP 本文不会用到,但是如果后续有升级合约的需求的话需要提供
export const PACKAGE = "0x5780ec9a0ab44c86b957855eab35fa3e0dacb71d683109e40c50f94fca2f411b"
export const UPGRADE_CAP = "0xb6222d0ab94ca5388b0722de9a4aab7ad10ff74bbe91a00d7c8fd1698d185c95"
export const RANDOM = "0x8"
export const FUNCTION = `${PACKAGE}::game::play`
export const EVENT = `${PACKAGE}::game::RandomEvent`

前端与合约交互

根据 Sui dApp 教学文档,我们在page.tsx中添加以下代码:

const {mutateAsync: signAndExecuteTransaction} = useSignAndExecuteTransaction({execute: async ({bytes, signature}) =>await suiClient.executeTransactionBlock({transactionBlock: bytes,signature,options: {showRawEffects: true,showEvents: true},})
});

通过这段代码我们将得到一个async的函数signAndExecuteTransaction,这将是我们唤起钱包签署交易的入口;
根据useSignAndExecuteTransaction内部的定义,在链上交易成功后会返回带有events的信息,我们所需要的随机数就在里面。
如果对返回值不关心,甚至可以直接const { mutate: signAndExecuteTransaction } = useSignAndExecuteTransaction();来获取入口。

假设我们已经实现了一个名为play的函数,它接受一个参数,就是上面的这个交易入口,我们将在我方游戏区域点击事件中触发:

const clickChoose = async (e: MouseEvent<HTMLImageElement>) => {console.log(e.currentTarget.alt);const chosen = await play(signAndExecuteTransaction);console.log(chosen);
}

play函数我们放在lib/contracts目录下,搭好最基本的函数框架:

export default async function play(signAndExecuteTransaction) {
}

首先需要为这个参数指定类型,回到page.tsx将鼠标悬停在这个定义好的交易入口上,发现它的类型是:
UseMutateAsyncFunction<SuiTransactionBlockResponse, UseSignAndExecuteTransactionError, UseSignAndExecuteTransactionArgs, unknown>
于是:

export default async function play(signAndExecuteTransaction: UseMutateAsyncFunction<SuiTransactionBlockResponse, UseSignAndExecuteTransactionError, UseSignAndExecuteTransactionArgs, unknown>) {
}

显然,全是报错,因为很多该导入的没有导入。当然可以在当前文件导入,但如果不止有这一个交易,就需要在各个文件重新做一遍类似的操作,为了更便于管理,我们在lib/contracts下新建一个type.ts,专门用来放交易过程中可能用到的(通用)东西。
(可能会报错包不存在,通过pnpm add -D <name>或者等价的命令将其添加)

import {UseMutateFunction, UseMutateAsyncFunction} from "@tanstack/react-query";
import {SuiTransactionBlockResponse} from "@mysten/sui/client";
import type { SuiSignAndExecuteTransactionInput } from '@mysten/wallet-standard';
import { PartialBy } from "@mysten/dapp-kit/dist/cjs/types/utilityTypes";
import { WalletFeatureNotSupportedError, WalletNoAccountSelectedError, WalletNotConnectedError } from "@mysten/dapp-kit/dist/cjs/errors/walletErrors";
import {Transaction} from "@mysten/sui/transactions";type UseSignAndExecuteTransactionError = WalletFeatureNotSupportedError | WalletNoAccountSelectedError | WalletNotConnectedError | Error;
type UseSignAndExecuteTransactionArgs = PartialBy<Omit<SuiSignAndExecuteTransactionInput, 'transaction'>, 'account' | 'chain'> & {transaction: Transaction | string;
};export type {UseMutateFunction,UseMutateAsyncFunction,SuiTransactionBlockResponse,UseSignAndExecuteTransactionError,UseSignAndExecuteTransactionArgs,}

将上面导出的东西,导入play.ts,剩下要做的就是实现这个函数。同样的,根据Sui dApp教学,依葫芦画瓢:

const tx = new Transaction();
tx.moveCall({target: FUNCTION,arguments: [tx.object(RANDOM)],
});
const response = await signAndExecuteTransaction({transaction: tx});

我们新建了一个交易,内容是调用FUNCTION(我们事先在config/key.ts中定义好了),调用的这个链上函数有一个参数,是一个Random对象,通过tx.object(<Object Address>)来将随机数的对象地址0x8转化为对象。
在唤起钱包签署交易的入口里传入这一笔交易,返回的内容存储在response中。
接下来,只需要在其中找到(对应的)EVENT,再将其中存储的chosen返回即可:

let chosen = 0;
response.events?.forEach(event => {if (event.type === EVENT) {chosen = (event.parsedJson as ParsedJson).chosen;}
});
return chosen;

将项目跑起来,测试是否如我所想的那样执行:

在这里插入图片描述

输赢结算

首先,在lib目录下新建一个games目录,里面建一个checkIsWinner.ts用来编写判断输赢的函数。

合约随机出的1~3分别表示石头、剪刀、布,我们将前端的选择,也就是e.currentTarget.alt按照同样的规则转化成数字,从中不难发现一个规律:
石头1 > 剪刀2 > 布3,当我们的选择和链上的随机数的差的绝对值小于等于1的时候,数字小的那一方获胜,否则,将两个数都对3取余数后再执行同样的判断,也就是布3 % 3 = 0 > 石头1 % 3 = 1 > 剪刀2 % 3 = 2
可以证明,这个取余数的过程最多进行一次就必定会判成胜负,于是,编码如下:

function strToNumber(str: string) {if (str === "rock")return 1;if (str === "scissors")return 2;return 3;
}function check(my: number, move: number) {if (Math.abs(my - move) > 1)return check(my % 3, move % 3);return my < move;
}export default function checkIsWinner(my_choice: string, move_choice: number) {return check(strToNumber(my_choice), move_choice);
}

合约交易成功后,在page.tsx中调用该函数。为了让胜负结算标签受该值控制,我们需要用useState来新建一个变量,同时为该标签绑定一个重开功能的点击事件,重开功能很容易实现,只需要将控制状态的值设为初始值即可:

const [isWinner, setIsWinner] = useState<boolean | null>(null);
const clickChoose = async (e: MouseEvent<HTMLImageElement>) => {const my_chosen = e.currentTarget.alt;const chosen = await play(signAndExecuteTransaction);setIsWinner(checkIsWinner(my_chosen, chosen));
}const gameAgain = () => {setIsWinner(null);setIsPlaying(false);
}{isWinner !== null&&<divclassName="absolute w-full top-1/2 -translate-y-1/2 cursor-pointer animate-bounce text-center"onClick={gameAgain}>{isWinner ? "Congratulations, you’ve got it all!" : "No! Everyone believes you will win next time!"}</div>
}

在这里插入图片描述

至此,大体功能已全部实现,当然,这是在没有任何误操作(比如未连接钱包开始游戏等)的前提下,同时,最后那只不断闪动的手也应该有一个最终归宿。不过这剩下的大多都是优化或者美化的环节,这里就不再详细阐述。你也可以根据自己的喜好将这一段留白增添一份天马行空的创意。

加入组织,共同进步!

  • Sui 中文开发群(TG)
  • M o v e \mathit{Move} Move 语言学习交流群: 79489587

相关文章:

Next.js + Move 石头剪刀布

rock-paper-scissors 写在前面 本地 源码&#xff1a;https://github.com/zcy1024/SuiStudy/tree/main/rock-paper-scissors # 或其它等价的命令来安装依赖并将项目跑起来 pnpm install pnpm run dev # http://localhost:3000/在线&#xff08;如果没过期的话&#xff09; …...

[面试]关于Redis 的持久化你了解吗

Redis的持久化是指Redis服务器在关闭或重启时&#xff0c;将内存中的数据保存到磁盘上的一种机制。Redis支持多种持久化方式。 一、RDB&#xff08;Redis Database&#xff09;持久化 RDB持久化是Redis默认采用的持久化方式&#xff0c;它将Redis在某个时间点的数据保存到磁盘上…...

Systemd:tmpfiles

Systemd提供了一个结构化的可配置方法来管理临时文件和目录,即systemd-tmpfiles,可以创建、删除和管理临时文件的服务。 $ systemctl list-units --all | grep systemd-tmpfilessystemd-tmpfiles-clean.service load…...

【Flutter 内嵌 android 原生 View以及相互跳转】

Flutter 内嵌 android 原生 View以及相互跳转 一. 内嵌android 原生View二、android 与 flutter 相互跳转 一. 内嵌android 原生View 在android 工程的包名下&#xff0c;也可在MainActivity创建 android 原生view &#xff0c;继承PlatformView // 1.自定义textview public st…...

python externally-managed-environment 外部管理环境

https://realpython.com/python-virtual-environments-a-primer/?refyaolong.net#why-do-you-need-virtual-environments 简而言之&#xff0c; pip 默认会将您安装的所有外部包放置在 Python 安装路径/site-packages/ 的文件夹中一些Linux 和 macOS操作系统 预装了内部的 P…...

前端 | MYTED单篇TED词汇学习功能优化

文章目录 &#x1f4da;实现效果&#x1f407;before&#x1f407;after &#x1f4da;模块实现解析&#x1f407;html&#x1f407;css&#x1f407;javascript &#x1f4da;实现效果 &#x1f407;before 点击TED单篇词汇表按钮&#xff0c;选择对应TED打卡号&#xff0c;…...

64 mysql 的 表锁

前言 我们这里来说的就是 我们在 mysql 这边常见的 几种锁 行共享锁, 行排他锁, 表意向共享锁, 表意向排他锁, 表共享锁, 表排他锁 我们前面了解了行共享锁, 行排他锁, 表意向共享锁, 表意向排他锁 等等相关 我们这里 来看一下 表共享锁, 表排他锁 的获取, 以及 和 其他表级…...

【计网不挂科】计算机网络期末考试——【选择题&填空题&判断题&简述题】题库(1)

前言 大家好吖&#xff0c;欢迎来到 YY 滴计算机网络 系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 本博客主要内容&#xff0c;收纳了一部门基本的计算机网络题目&#xff0c;供yy应对期中考试复习。大家可以参考 欢迎订阅 YY滴其他专栏&#xff01;…...

ajax关于axios库的运用小案例

AJAX案例 图书管理 四大功能&#xff1a; 展示图书删除图书编辑图书信息新增图书 步骤 1.bootstrap弹窗来实现新增和编辑图书时出现的弹窗 有两种方案&#xff1a; a.可以用自带的属性来进行弹窗的显示和隐藏 b.可以通过JS进行控制&#xff0c;此操作可以进行自定义&am…...

微搭低代码入门01变量

目录 1 变量的定义2 变量的赋值3 变量的类型4 算术运算符5 字符串的连接6 模板字符串7 检查变量的类型8 解构赋值8.1 数组的解构赋值8.2 对象的解构赋值 9 类型转换9.1 转换为字符串9.2 转换为数字9.3 转换为布尔值 总结 好些零基础的同学&#xff0c;在使用低代码的时候&#…...

盘点2024年10款视频剪辑,哪款值得pick!!

在这个短视频盛行的时代&#xff0c;如何让我们的故事更生动有趣呢&#xff1f;那就要对短视频进行修饰了。这就需要借助视频剪辑工具&#xff1a;而一款好的工具不仅仅是视频的“美颜”&#xff0c;更是创意的灵魂所在&#xff01;想象一下&#xff0c;运用一款功能齐全的剪辑…...

苹果手机照片批量删除:一键清理,释放空间

在数字化时代&#xff0c;iPhone不仅是我们沟通的桥梁&#xff0c;也是记录生活的重要工具。然而&#xff0c;随着时间的积累&#xff0c;手机中的照片数量不断增加&#xff0c;不仅占用大量存储空间&#xff0c;也让设备变得缓慢。苹果手机照片批量删除成为了一个普遍的需求。…...

《AI 大模型:重塑软件开发新生态》

《AI 大模型&#xff1a;重塑软件开发新生态》 一、AI 大模型引领软件开发新潮流二、AI 大模型在软件开发中的优势&#xff08;一&#xff09;提高开发效率&#xff08;二&#xff09;减少错误与提升质量&#xff08;三&#xff09;激发创新与拓展功能 三、AI 大模型在软件开发…...

uniapp(API-Promise 化)

一、异步的方法&#xff0c;如果不传入 success、fail、complete 等 callback 参数&#xff0c;将以 Promise 返回数据异步的方法&#xff0c;且有返回对象&#xff0c;如果希望获取返回对象&#xff0c;必须至少传入一项 success、fail、complete 等 callback 参数&#xff0c…...

【考研数学 - 数二题型】考研数学必吃榜(数二)

数学二 suhan, 2024.10 文章目录 数学二一、函数❗1.极限1.1求常见极限1.2求数列极限1.2.1 n项和数列极限1.2.2 n项连乘数列极限1.2.3 递推关系定义的数列极限 1.3确定极限式中的参数1.4无穷小量阶的比较 2.连续2.1判断是否连续&#xff0c;不连续则判断间断点类型2.2证明题 二…...

Redis生产问题(缓存穿透、击穿、雪崩)——针对实习面试

目录 Redis生产问题什么是缓存穿透&#xff1f;如何解决缓存穿透&#xff1f;什么是缓存击穿&#xff1f;如何解决缓存击穿&#xff1f;缓存穿透和缓存击穿有什么区别&#xff1f;什么是缓存雪崩&#xff1f;如何解决缓存雪崩&#xff1f; Redis生产问题 什么是缓存穿透&#x…...

android openGL中模板测试、深度测试功能的先后顺序

目录 一、顺序 二、模板测试 1、概念 2、工作原理 3、关键函数 三、深度测试 1、概念 2、工作原理 3、关键函数 三、模板测试和深度测试的先后顺序 一、顺序 在Android OpenGL中&#xff0c;模板测试&#xff08;Stencil Testing&#xff09;是在深度测试&#xff0…...

CCF PTA 编程培训师资认证2021年7月真题- C++兑换礼品

【题目描述】 小零和小壹是两个爱玩游戏的小孩&#xff0c;他俩平时最擅长的是解谜游戏&#xff0c;可今天 遇到了一个有点难的算法问题&#xff0c;希望能得到你的帮助。 他们面对的是一个电子装置&#xff0c;正面有 n 个排成一列的按钮&#xff0c;按钮上贴着编号 1~n 号的…...

火山引擎云服务docker 安装

安装 Docker 登录云服务器。 执行以下命令&#xff0c;添加 yum 源。 yum update -y yum install epel-release -y yum clean all yum list依次执行以下命令&#xff0c;添加Docker CE镜像源。更多操作请参考Docker CE镜像。 # 安装必要的一些系统工具 sudo yum install -y yu…...

【taro react】 ---- 常用自定义 React Hooks 的实现【六】之类渐入动画效果的轮播

1. 效果 2. 场景 css 效果实现:可以看到效果图中就是一个图片从小到大的切换动画效果,这个效果很简单,使用 css 的 transform 的 scale 来实现图片的从小到大的效果,切换就更加简单了,不管是 opacity 还是 visibility 都可以实现图片的隐藏和显示的切换。React.Children.m…...

基础算法练习--滑动窗口(已完结)

算法介绍 滑动窗口算法来自tcp协议的一种特性,它的高效使得其也变成了算法题的一种重要考点.滑动窗口的实现实际上也是通过两个指针前后遍历集合实现,但是因为它有固定的解题格式,我将其单独做成一个篇章. 滑动窗口的解题格式: 首先,定义两个指针left和right,与双指针不同的…...

深度学习经典模型之ZFNet

1 ZFNet 1.1 模型介绍 ​ ZFNet是由 M a t t h e w Matthew Matthew D . Z e i l e r D. Zeiler D.Zeiler和 R o b Rob Rob F e r g u s Fergus Fergus在AlexNet基础上提出的大型卷积网络&#xff0c;在2013年ILSVRC图像分类竞赛中以11.19%的错误率获得冠军&#xff08;实际…...

Linux系统-ubuntu系统安装

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注作者&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 这是Linux进阶部分的最后一大章。讲完这一章以后&#xff0c;我们Linux进阶部分讲完以后&#xff0c;我们的Linux操作部分就…...

2-Ubuntu/Windows系统启动盘制作

学习目标&#xff1a; 掌握使用Win32DiskImager、Rufus等工具制作系统启动盘的基本步骤。独立将ISO镜像文件写入USB闪存驱动器&#xff0c;确保在需要时顺利安装或修复系统。通过学习如何选择正确的源文件和目标驱动器&#xff0c;理解启动盘的使用场景和注意事项&#xff0c;…...

你使用过哪些MySQL中复杂且使用不频繁的函数?

在MySQL中&#xff0c;除了常用的SELECT、INSERT、UPDATE等基本操作外&#xff0c;还有许多复杂且功能强大的函数&#xff0c;它们能够处理各种复杂的数据处理需求。这些函数虽然在日常开发中可能不常使用&#xff0c;但在特定场景下却能够发挥巨大的作用。下面&#xff0c;我将…...

Redis-07 Redis哨兵

操作实现 此处应该6台虚拟机&#xff0c;其中3台是哨兵&#xff0c;但因为内存限制没有那么多 1.将sentinel文件拷贝到/myredis目录下 2.sentinel.conf文件重要参数 新建配置文件sentinel26379.conf sentinel26380.conf sentinel26381.conf bind 0.0.0.0 daemonize yes pr…...

7.qsqlquerymodel 与 qtableview使用

目录 qtableview 委托QStyledItemDelegateQAbstractItemDelegateCheckBoxItemDelegate使用qtableview控制列宽&#xff0c;行高&#xff0c;隐藏拖拽行列 qtableview 委托 //设置单元格委托 void setItemDelegate(QAbstractItemDelegate *delegate); QAbstractItemDelegate *it…...

状态模式(State Pattern)详解

1. 引言 在很多软件系统中&#xff0c;对象的行为往往依赖于其内部状态&#xff0c;而状态的变化又会影响对象的行为。状态模式&#xff08;State Pattern&#xff09;为解决这一问题提供了一种优雅的方法。通过将状态的行为封装到独立的状态对象中&#xff0c;可以使得对象在…...

ajax微信静默登录不起效不跳转问题

问题描述&#xff1a; 今天通过ajax调用方式做微信静默登录&#xff0c;发现本地可以跳转&#xff0c;到线上地址死活都不跳转&#xff0c;就像没起作用一般&#xff0c;经许久排查发现&#xff0c;是因为https和http域名的问题&#xff0c;线上只配置了http域名&#xff0…...

参数估计理论

估计理论的主要任务是在某种信号假设下&#xff0c;估算该信号中某个参数&#xff08;比如幅度、相位、达到时间&#xff09;的具体取值。 参数估计&#xff1a;先假定研究的问题具有某种数学模型&#xff0c; 如正态分布&#xff0c;二项分布&#xff0c;再用已知类别的学习样…...