Ответ 1
Ваш вопрос технически заключается в том, как завершить это с помощью правил безопасности, но поскольку это немного проблема XY, и ни одна из других возможностей не была исключена, Я также займусь некоторыми из них.
Я буду делать много предположений, так как для ответа на этот вопрос на самом деле требуется полностью определенный набор правил, которым необходимо следовать, и на самом деле это вопрос внедрения целого приложения (увеличение балла является результатом правила логики игры, а не простая математическая проблема).
Всего баллов у клиента
Возможно, самый простой ответ на эту загадку - просто не иметь общий балл. Просто возьмите список игроков и суммируйте их вручную.
Если это может быть полезно:
- список игроков составляет сотни или меньше.
- данные игрока соответственно малы (не 500k каждый)
Как это сделать:
var ref = new Firebase(URL);
function getTotalScore(gameId, callback) {
ref.child('app/games/' + gameId + '/players').once('value', function(playerListSnap) {
var total = 0;
playerListSnap.forEach(function(playerSnap) {
var data = playerSnap.val();
total += data.totalScore || 0;
});
callback(gameId, total);
});
}
Использовать привилегированного работника для обновления оценки
Очень сложный и простой подход (поскольку он требует только того, чтобы правила безопасности были установлены как-то вроде ".write": "auth.uid === 'SERVER_PROCESS'"
), было бы использовать серверный процесс, который просто контролирует игры и накапливает итоговые значения. Это, вероятно, самое простое решение для правильного и простого в обслуживании, но имеет недостаток в необходимости использования другой рабочей части.
Если это может быть полезно:
- вы можете развернуть службу Heroku или развернуть файл .js на webscript.io
- дополнительная ежемесячная подписка в диапазоне $5 $30 не является нарушителем транзакций
Как это сделать:
Очевидно, это связано с большим количеством дизайна приложения, и есть различные уровни, над которыми это должно быть достигнуто. Давайте сосредоточимся только на закрытии игр и подсчете лидеров, так как это хороший пример.
Начните с разбивки кода подсчета на свой собственный путь, например
/scores_entries/$gameid/$scoreid = < player: ..., score: ... >
/game_scores/$gameid/$playerid = <integer>
Теперь отслеживайте игры, чтобы увидеть, когда они закрываются:
var rootRef = new Firebase(URL);
var gamesRef = rootRef.child('app/games');
var lbRef = rootRef.child('leaderboards');
gamesRef.on('child_added', watchGame);
gamesRef.child('app/games').on('child_remove', unwatchGame);
function watchGame(snap) {
snap.ref().child('status').on('value', gameStatusChanged);
}
function unwatchGame(snap) {
snap.ref().child('status').off('value', gameStatusChanged);
}
function gameStatusChanged(snap) {
if( snap.val() === 'CLOSED' ) {
unwatchGame(snap);
calculateScores(snap.name());
}
}
function calculateScores(gameId) {
gamesRef.child(gameId).child('users').once('value', function(snap) {
var userScores = {};
snap.forEach(function(ss) {
var score = ss.val() || 0;
userScores[ss.name()] = score;
});
updateLeaderboards(userScores);
});
}
function updateLeaderboards(userScores) {
for(var userId in userScores) {
var score = userScores[userId];
lbRef.child(userId).transaction(function(currentValue) {
return (currentValue||0) + score;
});
}
}
Использовать путь аудита и правила безопасности
Это, конечно, будет самым сложным и трудным из доступных вариантов.
Если это может быть полезно:
- когда мы отказываемся использовать любую другую стратегию, связанную с серверным процессом.
- когда ужасно беспокоились об обманах игроков.
- когда у нас есть много дополнительного времени для записи
Очевидно, я склонен к этому подходу. Прежде всего потому, что очень трудно получить право и требует много энергии, которая может быть заменена небольшими денежными вложениями.
Получение этого права требует тщательного изучения каждого индивидуального запроса на запись. Есть несколько очевидных точек для защиты (возможно, больше):
- Написание любого игрового события, которое включает в себя приращение счета
- Написание общей суммы для игры на пользователя
- Написание общей суммы игры в таблице лидеров
- Запись каждой записи аудита
- Обеспечение лишних игр не может быть создано и изменено "на лету" только для увеличения баллов.
Вот некоторые основные основы для обеспечения каждой из этих точек:
- используйте контрольный журнал, в котором пользователи могут добавлять (не обновлять или удалять) записи
- подтвердите, что каждая запись аудита имеет приоритет, равный текущей временной отметке
- подтвердите, что каждая запись аудита содержит достоверные данные в соответствии с текущим состоянием игры
- использовать записи аудита при попытке увеличения итоговых итогов
Пусть, например, безопасно обновляет таблицу лидеров. Будем считать следующее:
- рейтинг пользователя в игре действителен.
- пользователь создал запись аудита, скажем, leaderboard_audit/$userid/$gameid, с текущей меткой времени в качестве приоритета и оценки в качестве значения
- каждая запись пользователя существует в таблице лидеров
- только пользователь может обновить свой собственный счет.
Итак, наша предполагаемая структура данных:
/games/$gameid/users/$userid/score
/leaderboard_audit/$userid/$gameid/score
/leaderboard/$userid = { last_game: $gameid, score: <int> }
Вот как работает наша логика:
- оценка игры равна
/games/$gameid/users/$userid/score
- запись аудита создается в
/leaderboard_audit/$userid/games_played/$gameid
- значение в
/leaderboard_audit/$userid/last_game
обновляется в соответствии с$gameid
- таблица лидеров обновляется на сумму, равную
last_game
записи аудита
И вот настоящие правила:
{
"rules": {
"leaderboard_audit": {
"$userid": {
"$gameid": {
// newData.exists() ensures records cannot be deleted
".write": "auth.uid === $userid && newData.exists()",
".validate": "
// can only create new records
!data.exists()
// references a valid game
&& root.child('games/' + $gameid).exists()
// has the correct score as the value
&& newData.val() === root.child('games/' + $gameid + '/users/' + auth.uid + '/score').val()
// has a priority equal to the current timestamp
&& newData.getPriority() === now
// is created after the previous last_game or there isn't a last_game
(
!root.child('leaderboard/' + auth.uid + '/last_game').exists() ||
newData.getPriority() > data.parent().child(root.child('leaderboard/' + auth.uid + '/last_game').val()).getPriority()
)
"
}
}
},
"leaderboard": {
"$userid": {
".write": "auth.uid === $userid && newData.exists()",
".validate": "newData.hasChildren(['last_game', 'score'])",
"last_game": {
".validate": "
// must match the last_game entry
newData.val() === root.child('leaderboard_audit/' + auth.uid + '/last_game').val()
// must not be a duplicate
newData.val() !== data.val()
// must be a game created after the current last_game timestamp
(
!data.exists() ||
root.child('leaderboard_audit/' + auth.uid + '/' + data.val()).getPriority()
< root.child('leaderboard_audit/' + auth.uid + '/' + newData.val()).getPriority()
)
"
},
"score": {
".validate": "
// new score is equal to the old score plus the last_game score
newData.val() === data.val() +
root.child('games/' + newData.parent().child('last_game').val() + '/users/' + auth.uid + '/score').val()
"
}
}
}
}
}