This commit is contained in:
yaoyanwei
2025-08-04 16:25:38 +08:00
parent 8d542ea201
commit 4b2bb35c20
46 changed files with 5128 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
const MatchModel = require('./matches.model');
const MatchTool = require('./matches.tool');
const UserModel = require('../users/users.model');
const DateTool = require('../tools/date.tool');
const config = require('../config');
exports.addMatch = async(req, res) => {
var tid = req.body.tid;
var players = req.body.players;
var ranked = req.body.ranked ? true : false;
var mode = req.body.mode || "";
if (!tid || !players || !Array.isArray(players) || players.length != 2)
return res.status(400).send({ error: "Invalid parameters" });
if (mode && typeof mode !== "string")
return res.status(400).send({ error: "Invalid parameters" });
var fmatch = await MatchModel.get(tid);
if(fmatch)
return res.status(400).send({error:"Match already exists: " + tid});
var player0 = await UserModel.getByUsername(players[0]);
var player1 = await UserModel.getByUsername(players[1]);
if(!player0 || !player1)
return res.status(404).send({error:"Can't find players"});
if(player0.id == player1.id)
return res.status(400).send({error:"Can't play against yourself"});
var match = {};
match.tid = tid;
match.players = players;
match.winner = "";
match.completed = false;
match.ranked = ranked;
match.mode = mode;
match.start = Date.now();
match.end = Date.now();
match.udata = [];
match.udata.push(MatchTool.GetPlayerData(player0));
match.udata.push(MatchTool.GetPlayerData(player1));
var match = await MatchModel.create(match);
if(!match)
return res.status(500).send({error:"Unable to create match"});
res.status(200).send(match);
};
exports.completeMatch = async(req, res) => {
var matchId = req.body.tid;
var winner = req.body.winner;
if (!matchId || !winner)
return res.status(400).send({ error: "Invalid parameters" });
if(typeof matchId != "string" || typeof winner != "string")
return res.status(400).send({error: "Invalid parameters" });
var match = await MatchModel.get(matchId);
if(!match)
return res.status(404).send({error: "Match not found"});
if(match.completed)
return res.status(400).send({error: "Match already completed"});
var player0 = await UserModel.getByUsername(match.players[0]);
var player1 = await UserModel.getByUsername(match.players[1]);
if(!player0 || !player1)
return res.status(404).send({error:"Can't find players"});
match.end = Date.now();
match.winner = winner;
match.completed = true;
//Add Rewards
if(match.ranked)
{
match.udata[0].reward = await MatchTool.GainMatchReward(player0, player1, winner);
match.udata[1].reward = await MatchTool.GainMatchReward(player1, player0, winner);
match.markModified('udata');
}
//Save match
var uMatch = await match.save();
//Return
res.status(200).send(uMatch);
};
exports.getAll = async(req, res) => {
var start = req.query.start ? DateTool.tagToDate(req.query.start) : null;
var end = req.query.end ? DateTool.tagToDate(req.query.end) : null;
var matches = await MatchModel.list(start, end);
if(!matches)
return res.status(400).send({error: "Invalid Parameters"});
return res.status(200).send(matches);
};
exports.getByTid = async(req, res) => {
var match = await MatchModel.get(req.params.tid);
if(!match)
return res.status(404).send({error: "Match not found " + req.params.tid});
return res.status(200).send(match);
};
exports.getLatest = async(req, res) => {
var match = await MatchModel.getLast(req.params.userId);
if(!match)
return res.status(404).send({error: "Match not found for user " + req.params.userId});
return res.status(200).send(match);
};

87
matches/matches.model.js Normal file
View File

@@ -0,0 +1,87 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const matchSchema = new Schema({
tid: { type: String, index: true, default: "" },
players: [{type: String, default: []}],
winner: {type: String, default: ""},
completed: {type: Boolean, default: false},
ranked: {type: Boolean, default: false},
mode: { type: String, default: "" },
start: {type: Date, default: null},
end: {type: Date, default: null},
udata: [{ type: Object, _id: false }],
});
matchSchema.methods.toObj = function() {
var match = this.toObject();
delete match.__v;
delete match._id;
return match;
};
const Match = mongoose.model('Matches', matchSchema);
exports.get = async(matchId) => {
try{
var match = await Match.findOne({tid: matchId});
return match;
}
catch{
return null;
}
};
exports.getAll = async() => {
try{
var matches = await Match.find()
return matches || [];
}
catch{
return [];
}
};
exports.create = async(matchData) => {
const match = new Match(matchData);
return await match.save();
};
exports.list = async(startTime, endTime, winnerId, completed) => {
startTime = startTime || new Date(-8640000000000000);
endTime = endTime || new Date(8640000000000000);
var options = {};
if(startTime && endTime)
options.end = { $gte: startTime, $lte: endTime };
if(winnerId)
options.players = winnerId;
if(completed)
options.completed = true;
try{
var matches = await Match.find(options)
return matches || [];
}
catch{
return [];
}
};
exports.remove = async(matchId) => {
try{
var result = await Match.deleteOne({tid: matchId});
return result && result.deletedCount > 0;
}
catch{
return false;
}
};

34
matches/matches.routes.js Normal file
View File

@@ -0,0 +1,34 @@
const MatchesController = require('./matches.controller');
const MatchesTool = require('./matches.tool');
const AuthTool = require('../authorization/auth.tool');
const config = require('../config');
const ADMIN = config.permissions.ADMIN; //Highest permision, can read and write all users
const SERVER = config.permissions.SERVER; //Higher permission, can read all users
const USER = config.permissions.USER; //Lowest permision, can only do things on same user
exports.route = function (app) {
app.post('/matches/add', app.post_limiter, [
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(SERVER),
MatchesController.addMatch
]);
app.post('/matches/complete', app.post_limiter, [
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(SERVER),
MatchesController.completeMatch
]);
//-- Getter
app.get('/matches', [
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(SERVER),
MatchesController.getAll
]);
app.get('/matches/:tid', [
AuthTool.isValidJWT,
AuthTool.isPermissionLevel(USER),
MatchesController.getByTid
]);
};

71
matches/matches.tool.js Normal file
View File

@@ -0,0 +1,71 @@
const UserTool = require('../users/users.tool');
const config = require('../config.js');
var MatchTool = {};
MatchTool.calculateELO = (player_elo, opponent_elo, progress, won, lost) =>
{
var p_elo = player_elo || 1000;
var o_elo = opponent_elo || 1000;
var p_elo_log = Math.pow(10.0, p_elo / 400.0);
var o_elo_log = Math.pow(10.0, o_elo / 400.0);
var p_expected = p_elo_log / (p_elo_log + o_elo_log);
var p_score = won ? 1.0 : (lost ? 0.0 : 0.5);
progress = Math.min(Math.max(progress, 0.0), 1.0);
var elo_k = progress * config.elo_k + (1.0 - progress) * config.elo_ini_k;
var new_elo = Math.round(p_elo + elo_k * (p_score - p_expected));
return new_elo;
}
MatchTool.GetPlayerData = (player) =>
{
var data = {};
data.username = player.username;
data.elo = player.elo;
data.reward = {};
return data;
}
MatchTool.GainMatchReward = async(player, opponent, winner_username) => {
var player_elo = player.elo;
var opponent_elo = opponent.elo;
var won = winner_username == player.username;
var lost = winner_username == opponent.username;
//Rewards
var xp = won ? config.xp_victory : config.xp_defeat;
var coins = won ? config.coins_victory : config.coins_defeat;
player.xp += xp;
player.coins += coins;
//Match winrate
player.matches +=1;
if(won)
player.victories += 1;
else if (lost)
player.defeats += 1;
//Calculate elo
var match_count = player.matches || 0;
var match_progress = Math.min(Math.max(match_count / config.elo_ini_match, 0.0), 1.0);
var new_elo = MatchTool.calculateELO(player_elo, opponent_elo, match_progress, won, lost);
player.elo = new_elo;
player.save();
var reward = {
elo: player.elo,
xp: xp,
coins: coins
};
return reward;
};
module.exports = MatchTool;