Write card games with js (9)

Original link: https://www.xiejingyang.com/2023/06/03/%E7%94%A8js%E5%86%99%E5%8D%A1%E7%89%8C%E6%B8%B8% E6%88%8F%EF%BC%88%E4%B9%9D%EF%BC%89/

Hello everyone, long time no see again. Although I have been very busy recently, I still squeezed a lot of time to update this project. On the one hand, working on this project after intense work is equivalent to taking a break for me. On the other hand, I also hope to contribute to the open source business and share my thoughts on code with everyone to discuss and make progress together.

Let’s start with some small changes to the project.

  • Fixed multiple bugs
  • Vue2 upgraded to Vue3
  • The dependencies of the project are all upgraded to the latest and the debugging passed
  • Refactor the code structure and extract all the logic into separate files
  • The project adds logs and saves log files
  • Added the function of pairing with friends to designate room opening
  • The timeout function is more perfect
  • Use authorization in the project, use jwt as the session scheme

Next, I will share several of the changes in detail, including: designated pairing for room opening, timeout function, and jwt authorization. Then it will also share a newly added function in the game: professional skills.

Designated match for open house

The newly added function of matching with a friend’s specified room means that the user can choose to play one-on-one matching games with his friends instead of random matching. The main purpose of this function is to allow users to better play games with their friends and increase the sociality of the game.

The implementation of this function is relatively simple. The previous random matching created a room, but the room number cannot be specified, so this function only needs to slightly modify the previous random matching, and decompose it into two steps: create a room and join the room. On the basis of the previous random matching, the room number is returned to the user, but it will not be automatically added to the queue to be matched, and the room can only be entered through the room number. In this way, the user can enter the room number on the interface. If the room number is successfully matched, the subsequent process is the same as random matching.

 if (pvpGameMode === PvpMode.CREATE_ROOM) {  
    // create room let roomNumber = uuidv4();  
  
    // Prevent duplicate room numbers while (getRoomData(roomNumber)) {  
        roomNumber = uuidv4();  
    }  
    console.log("create room : " + roomNumber)  
  
    // r is the room number const seed = Math.floor(Math.random() * 10000);  
    // Create a room, be careful not to set startTime here, because you are still waiting for the opponent to join createRoomData(roomNumber, {  
        isPve,  
        gameMode: GameMode.PVP1,  
        seeds,  
        rand: seedrandom(seed),  
        round: 1  
    });  
    saveUserGameRoom(userId, roomNumber);  
  
    changeRoomData(roomNumber, 'one', {  
        userId, cardsId, socket, roomNumber, myMaxThinkTimeNumber: MAX_THINK_TIME_NUMBER  
    });  
  
    logger.info(`roomNumber:${roomNumber} userId:${userId} cardsId:${cardsId} pvp create`);  
  
    // While waiting, send the room number to the client, and let the client display it on the desktop socket.emit("WAIT", {  
        roomNumber  
    });  
    socket. join(roomNumber);  
}  
// pvp part // join the room if (pvpGameMode === PvpMode.JOIN_ROOM) {  
    if (!r) {  
        socket.emit("ERROR", "The room number cannot be empty");  
        return;  
    }  
  
    if (!getRoomData(r)) {  
        socket.emit("ERROR", "Room number not found");  
        return;  
    }  
  
    if (getRoomData(r).startTime) {  
        socket.emit("ERROR", "The room game has started");  
        return;  
    }  
  
    // r is the room number const roomNumber = r;  
    const memoryData = getRoomData(roomNumber);  
    saveUserGameRoom(userId, roomNumber);  
  
    changeRoomData(roomNumber, 'startTime', new Date());  
    changeRoomData(roomNumber, 'two', {  
        userId, cardsId, socket, roomNumber, myMaxThinkTimeNumber: MAX_THINK_TIME_NUMBER  
    });  
  
    logger.info(`roomNumber:${roomNumber} one userId1:${memoryData['one'].userId} cardsId1:${memoryData['one'].cardsId} two userId2:${userId} cardsId2:${cardsId } pvp start`);  
  
    socket. join(roomNumber);  
  
    memoryData['one'].socket.emit("START", {  
        roomNumber: roomNumber,  
        memberId: "one"  
    });  
  
    socket. emit("START", {  
        roomNumber: roomNumber,  
        memberId: "two"  
    });  
  
    initCard(roomNumber, memoryData['one']. cardsId, cardsId, memoryData['one']. userId, userId);  
  
    saveUserOperator(userId, { type: UserOperatorType.playPvp, with: memoryData['one'].userId, roomNumber: roomNumber });  
}  

Insert some front-end screenshots

front-end code

timeout function

Burning the rope is a very classic scene in Hearthstone. In this scenario, the player needs to complete the operation within the specified time, otherwise the timeout mechanism will be triggered. The timeout mechanism is one of the important mechanisms to ensure the fairness of the game. However, there is a fatal problem with the previous timeout mechanism, that is, it is implemented on the client side. This means that as long as the opponent closes the webpage, the timeout function will never be triggered, which greatly affects the fairness of the game.

In order to solve this problem, we put the timeout function on the server side for statistics. In this way, even if the opponent closes the web page, the timeout mechanism will still be triggered as expected. The idea of ​​implementation is also very simple: set a timer in the room, and reset the timer when switching players.

 // Timeout timer memoryData.timeoutId = setTimeout(() => {  
    logger.info(`${roomNumber} ${other} timeout ${memoryData[other].myMaxThinkTimeNumber} seconds, end the round automatically`);  
  
    endMyTurn(args, getSocket(roomNumber, other));  
    if (memoryData[other].timeoutTimes) {  
        memoryData[other].timeoutTimes += 1;  
  
        if (memoryData[other].timeoutTimes === 4) {  
            logger.info(`${roomNumber} ${other} timeout ${memoryData[other].myMaxThinkTimeNumber} seconds, reach 4 times, start timeout penalty`);  
            memoryData[other].myMaxThinkTimeNumber = memoryData[other].myMaxThinkTimeNumber / 2;  
        } else if (memoryData[other]. timeoutTimes >= 6) {  
            logger.info(`${roomNumber} ${other} timeout ${memoryData[other].myMaxThinkTimeNumber} seconds, reach 6 times, penalty to lose the game`);  
            giveUp(args, getSocket(roomNumber, other));  
        }  
    } else {  
        memoryData[other].timeoutTimes = 1;  
    }  
}, memoryData[other].myMaxThinkTimeNumber * 1000);  

At the same time, we refer to the time penalty mechanism of Hearthstone. If the overtime reaches 4 points, the time penalty will be given to the overtime player, and the thinking time will be halved. If there are 6 times overtime, the opponent will be judged to win directly.

In this way, players can enjoy a more fair and fair game experience with confidence. This improvement will help improve the overall quality of the game and the player’s gaming experience.

JWT authorization

First of all, what is JWT, JWT (JSON Web Token) is an open standard based on JSON, which is used to securely transmit information on the network, mainly for authentication and authorization. You can search for the specific details of jwt on the Internet.

The purpose of adding jwt authorization is to keep the website interface stateless and at the same time to perform authentication and authorization safely. In fact, authentication is not required in the game, because the game uses websocket for data interaction, as long as Once the server is successfully connected, it means that the websocket can continue to maintain the connection without modification.

In the latest version, we will use JWT for authorization, by validating the JWT token to ensure the user’s identity. The specific implementation steps are as follows:

  1. When users enter the website, they need to log in to verify their identity and obtain a JWT token. const token = jwt.sign({id : result._id}, JWTSecret, {expiresIn: 60 * 60 * 24 * 7});
  2. When users use our services, they need to carry JWT tokens in their requests so that the server can verify and authorize them. axios.defaults.headers.common['Authorization'] = "Bearer " + res.data.data.token;
  3. Validate the JWT token. After receiving each request, the server will verify the JWT token. app.use(jwt({ secret: JWTSecret, algorithms: ["HS256"] }).unless({ path: ["/users/login"] }))

professional skills

Thanks to the use of the js language, this language is very casual, and can obtain data and transfer data or methods very conveniently. Therefore, if you want to realize professional skills, the effect is the same as that of ordinary cards. The difficulty lies not in the code framework of professional skills, but in how to design more balanced professional skills.

If you simply imitate Hearthstone like a card, it may not match the current game settings, so I redesigned the professional skills. Based on this framework, professional skills are no longer a single skill, but can be Carry multi-professional skills, and according to the increase in skill consumption, the effect of skills will be more powerful.

My interface design for professional skills is as follows:

In this article, not all professional skills are designed, but a level that requires professional skills is placed in the level, which will reduce the complexity and only focus on the realization of professional skills.

First, the server will send the skills of both players to the client, and the client will display them on the table.

 this.gameData[first]['skillList'] = [  
    {  
        name: "Reading Books",  
        cost: 1,  
        isTarget: true,  
        targetType: TargetType. MY_TABLE_CARD,  
        description: 'Improve yourself by reading books, and choose to increase the attack power of a card on the table by 1 point',  
        onChooseTarget: cardEffectFactory.oneChooseCardAddAttack(1)  
    }  
]  

If there are multiple skills, you only need to send the entire skill list to the client.

When one of the skills is clicked, the client will perform a consumption calculation. If the cost is not enough, the skill will not be released. If the cost is enough, it will be handed over to the server for judgment. When all skill release conditions are met, it will be sent. The command for skill release is executed on the client side.

 onSkillClick(index, skill) {  
    if (skill. cost > this. gameData. myFee) {  
        this.showError("Insufficient cost");  
    } else {  
        this. currentSkillIndex = index;  
        if (skill. isTarget) {  
            let card = this.gameData.myCard[this.currentCardIndex];  
            if (skill. targetType === TargetType. MY_TABLE_CARD) {  
                this.chooseCardList = this.gameData.myTableCard.slice();  
            } else if (skill. targetType === TargetType. OTHER_TABLE_CARD) {  
                this.chooseCardList = this.gameData.otherTableCard.slice();  
            } else if (skill. targetType === TargetType. ALL_TABLE_CARD) {  
                this.chooseCardList =  
                    this.gameData.otherTableCard.slice()  
                        .concat(this.gameData.myTableCard.slice());  
            } else if (skill.targetType === TargetType.ALL_TABLE_CARD_FILTER_INCLUDE) { // All desktop cards, filter conditions include this.chooseCardList =  
                    this.gameData.otherTableCard  
                        .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1) && !i.isHide)  
                        .map(i => Object.assign({}, i, {name: i.name + "(enemy)"}))  
                        .concat(this.gameData.myTableCard.slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1)));  
            } else if (skill. targetType === TargetType. ALL_TABLE_CARD_FILTER_EXCLUDE) {  
                this.chooseCardList =  
                    this.gameData.otherTableCard  
                        .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1) && !i.isHide)  
                        .concat(this.gameData.myTableCard  
                            .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1)));  
            } else if (skill. targetType === TargetType. MY_TABLE_CARD_FILTER_INCLUDE) {  
                this.chooseCardList = this.gameData.myTableCard  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1));  
            } else if (skill. targetType === TargetType. MY_TABLE_CARD_FILTER_EXCLUDE) {  
                this.chooseCardList = this.gameData.myTableCard  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1));  
            } else if (skill. targetType === TargetType. OTHER_TABLE_CARD_FILTER_INCLUDE) {  
                this.chooseCardList = this.gameData.otherTableCard  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1));  
            } else if (skill. targetType === TargetType. OTHER_TABLE_CARD_FILTER_EXCLUDE) {  
                this.chooseCardList = this.gameData.otherTableCard  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1));  
            }  
  
            // Display the selection box this.chooseDialogType = ChooseDialogType.SKILL;  
            this.chooseDialogShow = true;  
        } else {  
            useSkillCommand.apply(this);  
        }  
    }  
},  

Server:

 /**  
 * use skill * @param args  
 * @param socket  
 */  
function useSkill(args, socket) {  
    let roomNumber = args.r, index = args.index, targetIndex = args.targetIndex, skill;  
    const memoryData = getRoomData(roomNumber);  
  
    let belong = getSocket(roomNumber, "one").id === socket.id ? "one" : "two"; // determine which player is currently playing the card let other = getSocket(roomNumber, "one").id !== socket.id ? "one" : "two";  
  
    if (index !== -1 && memoryData[belong]["skillList"][index].cost <= memoryData[belong]["fee"]) {  
        skill = memoryData[belong]["skillList"][index];  
  
        if (!skill. roundMaxUseTimes) {  
            skill.roundMaxUseTimes = 1;  
        }  
        if (!memoryData[belong]["useSkillRoundTimes"]) {  
            memoryData[belong]["useSkillRoundTimes"] = 0;  
        }  
        if (skill. roundMaxUseTimes <= memoryData[belong]["useSkillRoundTimes"]  
            || (skill.maxUseTimes && skill.maxUseTimes <= +memoryData[belong]["useSkillTimes"])) {  
            error(socket, "The number of times the skill can be used exceeds the upper limit");  
            return;  
        }  
  
        // To check whether the card is violated, the cast object attribute (isForceTarget) must be selected  
        let chooseCardList = [];  
        if (skill. isTarget) {  
            if (skill. targetType === TargetType. MY_TABLE_CARD) {  
                chooseCardList = memoryData[belong]["tableCards"];  
            } else if (skill. targetType === TargetType. OTHER_TABLE_CARD) {  
                chooseCardList = memoryData[other]["tableCards"];  
            } else if (skill. targetType === TargetType. ALL_TABLE_CARD) {  
                chooseCardList =  
                    memoryData[other]["tableCards"].slice()  
                        .concat(memoryData[belong]["tableCards"].slice());  
            } else if (skill. targetType === TargetType. ALL_TABLE_CARD_FILTER_INCLUDE) {  
                chooseCardList =  
                    memoryData[other]["tableCards"]  
                        .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1) && !i.isHide)  
                        .concat(memoryData[belong]["tableCards"]  
                            .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1)));  
            } else if (skill. targetType === TargetType. ALL_TABLE_CARD_FILTER_EXCLUDE) {  
                chooseCardList =  
                    memoryData[other]["tableCards"]  
                        .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1) && !i.isHide)  
                        .concat(memoryData[belong]["tableCards"]  
                            .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1)));  
            } else if (skill. targetType === TargetType. MY_TABLE_CARD_FILTER_INCLUDE) {  
                chooseCardList = memoryData[belong]["tableCards"]  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1));  
            } else if (skill. targetType === TargetType. MY_TABLE_CARD_FILTER_EXCLUDE) {  
                chooseCardList = memoryData[belong]["tableCards"]  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1));  
            } else if (skill. targetType === TargetType. OTHER_TABLE_CARD_FILTER_INCLUDE) {  
                chooseCardList = memoryData[other]["tableCards"]  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) !== -1));  
            } else if (skill. targetType === TargetType. OTHER_TABLE_CARD_FILTER_EXCLUDE) {  
                chooseCardList = memoryData[other]["tableCards"]  
                    .slice().filter(i => skill.filter.every(t => i.type.indexOf(t) === -1));  
            }  
            // You must choose the casting target, return an error if (chooseCardList.length === 0 && targetIndex === -1 && skill.isForceTarget) {  
                error(getSocket(roomNumber, belong), "Please select a target");  
                return;  
            }  
        }  
        memoryData[belong]["fee"] -= skill. cost;  
  
        let mySpecialMethod = getSpecialMethod(belong, roomNumber);  
  
        getSocket(roomNumber, belong). emit("USE_SKILL", {  
            index,  
            skill,  
            isMine: true,  
            myHero: extractHeroInfo(memoryData[belong]),  
            otherHero: extractHeroInfo(memoryData[other])  
        });  
        getSocket(roomNumber, other). emit("USE_SKILL", {  
            index,  
            skill,  
            isMine: false,  
            myHero: extractHeroInfo(memoryData[other]),  
            otherHero: extractHeroInfo(memoryData[belong])  
        })  
  
        if (skill. isTarget) {  
            skill.onChooseTarget({  
                myGameData: memoryData[belong],  
                otherGameData: memoryData[other],  
                source: skill,  
                chooseCard: chooseCardList[targetIndex],  
                effectIndex: args. effectIndex,  
                fromIndex: -1,  
                toIndex: targetIndex,  
                specialMethod: mySpecialMethod  
            });  
        }  
  
        if (skill && skill. onStart) {  
            skill.onStart({  
                myGameData: memoryData[belong],  
                otherGameData: memoryData[other],  
                source: skill,  
                specialMethod: mySpecialMethod  
            });  
        }  
  
        memoryData[belong]["useSkillTimes"]++;  
        memoryData[belong]["useSkillRoundTimes"]++;  
  
        checkCardDieEvent(roomNumber);  
    } else {  
        error(socket, 'Insufficient cost or skill not selected for use');  
    }  
  
    checkPvpWin(roomNumber);  
    checkPveWin(roomNumber);  
}  

next plan

Since the current AI is very strong and popular (I want to catch the heat), and since I want to do this project well, I will use AI to carry out a new design of the entire game.

And I hope to make a more user-friendly tool to make card battle games easier to make.

There are many things I want to do, but I have too little spare time. If you have any good ideas, please leave a message.

The post Writing card games with js (9) first appeared on Xieisabug .

This article is reproduced from: https://www.xiejingyang.com/2023/06/03/%E7%94%A8js%E5%86%99%E5%8D%A1%E7%89%8C%E6%B8%B8% E6%88%8F%EF%BC%88%E4%B9%9D%EF%BC%89/
This site is only for collection, and the copyright belongs to the original author.