In my previous question I asked how could the Redis client be improved. Here I will ask how can the Game data Client can be improved. Game Data client sits on top of Redis Client and uses datatypes from datape package
Here is the code:
package gamedatamanager
import (
"log"
"reflect"
"encoding/json"
"sort"
"gogameserver/util"
dt "gogameserver/datatypes"
rcl "gogameserver/redisclient"
)
const REDIS_NIL string = "redis: nil"
type GameManager struct {
rc *rcl.RedisClient
}
func New() (gm * GameManager) {
return &GameManager{
rc: rcl.New(),
}
}
func (gm * GameManager) DelKey(key string) (int64, bool) {
redisRet, err := gm.rc.DelKey(key)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("\nERROR:DelKey| Key: %s not found, err:%v", key, err)
return -1, false
} else {
return redisRet, true
}
}
func (gm * GameManager) GetPlayerData(gameName string, playerId string) (string, bool) {
gameName = gameName+playerId
playerDataStr, err := gm.rc.GetVal(gameName)
if err != nil && err.Error() == REDIS_NIL {
go log.Printf("\nERROR:GetPlayerData: Game %s, playerId %s not found, err:%v", gameName, playerId, err)
return "player not found", false
} else {
return playerDataStr, true
}
}
func (gm * GameManager) DelPlayerData(gameName string, playerId string) (int64, bool) {
gm.DelPlayerName(gameName, playerId)
gameName = gameName+playerId
redisRet, err := gm.rc.DelKey(gameName)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("\nERROR:DelPlayerData: Game %s, playerId %s not found, err:%v", gameName, playerId, err)
return -1, false
} else {
return redisRet, true
}
}
// store player data
func (gm * GameManager) StorePlayerData(gameName string, playerData dt.PlayerData) (bool){
gm.StorePlayerName(gameName, playerData.N, playerData.I)
gameName = gameName+playerData.I
err := gm.rc.SaveKeyValForever(gameName, dt.Str(playerData))
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("\nERROR:StorePlayerData: Game %s, playerData %v, err:%v", gameName, playerData, err)
return false
} else {
go log.Printf("\nInfo: Success StorePlayerData: Game %s, playerData %v", gameName, playerData)
return true
}
}
// store player new score
func (gm * GameManager) StorePlayerScore(gameName string, score float64, playerId string) (bool){
currHiScore, err := gm.GetPlayerHighScore(gameName, playerId)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("ERROR:StorePlayerScore: Game %s, playerId %s not found. err:'%s'", gameName, playerId, err.Error() )
return false
} else {
hiScoretoday, _ := gm.GetPlayerScoreOnDay(gameName, playerId , 0)
if hiScoretoday < score {
gm.StorePlayerScoreDaily(gameName , score , playerId)
}
if currHiScore < score {
pDataStr, found := gm.GetPlayerData(gameName, playerId)
if !found {
return false
}
pData := dt.JsonFromStr(pDataStr)
pData.A = score
// a go routine to update
gm.StorePlayerData(gameName, pData)
redisRet, redisErr := gm.rc.AddToSet(gameName, score, playerId)
if redisErr != nil && redisErr.Error() != REDIS_NIL {
go log.Printf("Error:AddToSet: SUCESS gameName:%s, score:%f, playerId:%s, redisErr:%v", gameName, score, playerId, redisErr)
return false
}
go log.Printf("Info:StorePlayerScore: SUCESS currHiScore:%.6f, newScore:%.6f, Game %s, playerId %s, retcode:%d", currHiScore, score, gameName, playerId, redisRet)
} else {
go log.Printf("Info:StorePlayerScore: Already high currHiScore:%.6f, newScore:%.6f, Game %s, playerId %s", currHiScore, score, gameName, playerId)
}
return true
}
}
func getKeyForName(gameName string, playerId string) string {
return gameName+playerId+"Name"
}
// store player new score
func (gm * GameManager) StorePlayerName(gameName string, playerName string, playerId string) (bool){
key := getKeyForName(gameName, playerId)
err := gm.rc.SaveKeyValForever(key, playerName)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("\nERROR:StorePlayerName: Game %s, playerName %s, err:%v", gameName, playerName, err)
return false
} else {
go log.Printf("\nInfo: Success StorePlayerName: Game %s, playerName %s", gameName, playerName)
return true
}
}
// store player new score
func (gm * GameManager) GetPlayerName(gameName string, playerId string) (string, bool){
key := getKeyForName(gameName, playerId)
playerName, err := gm.rc.GetVal(key)
if err != nil && err.Error() == REDIS_NIL {
go log.Printf("\nERROR:GetPlayerName: Game %s, playerId %s not found, err:%v", gameName, playerId, err)
return "playerName not found", false
} else {
return playerName, true
}
}
// store player new score
func (gm * GameManager) DelPlayerName(gameName string, playerId string) (int64, bool){
key := getKeyForName(gameName, playerId)
redisRet, err := gm.rc.DelKey(key)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("\nERROR:DelPlayerName: Game %s, playerId %s not found, err:%v", gameName, playerId, err)
return -1, false
} else {
return redisRet, true
}
}
// delete player score
func (gm * GameManager) DeletePlayerScore(gameName string, playerId string) (bool){
redisRet, redisErr := gm.rc.RemScore(gameName, playerId)
if redisErr != nil && redisErr.Error() != REDIS_NIL {
go log.Printf("Error:DeletePlayerScore: SUCESS gameName:%s, playerId:%s, redisErr:%v", gameName, playerId, redisErr)
return false
} else {
go log.Printf("Info :DeletePlayerScore: SUCESS gameName:%s, playerId:%s, redisRet:%d", gameName, playerId, redisRet)
}
return true
}
// get player rank
func (gm * GameManager) GetPlayerRank(gameName string, playerId string) (int64) {
rank, err := gm.rc.GetRank(gameName, playerId)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("Error:GetPlayerRank: Game %s, playerId %s", gameName, playerId)
return -1;
} else {
return rank+1;
}
}
// get top x
func (gm * GameManager) GetTopPlayers(gameName string, topCount int64) (string) {
playeScores := make([]dt.PlayerScore, 0)
topResults, err := gm.rc.GetTop(gameName, topCount)
if err != nil && err.Error() != REDIS_NIL{
go log.Printf("Error:GetTopPlayers: Game %s", gameName)
return "json error"
}
topResultsVal := reflect.ValueOf(topResults)
resultCount := topResultsVal.Len()
for i:=0; i<resultCount; i++ {
_score,_ := topResultsVal.Index(i).Field(0).Interface().(float64)
_pid,_ := topResultsVal.Index(i).Field(1).Interface().(string)
_name,_ := gm.GetPlayerName(gameName, _pid)
playeScores = append(playeScores, dt.PlayerScore{_name, _score})
}
sort.Sort(dt.ByScoreRev(playeScores))
if topCount < int64(len(playeScores)) {
playeScores = playeScores[:topCount]
}
go log.Printf("Info: GetTopPlayers: top %d playeScores: %v", topCount, playeScores)
b, jerr := json.Marshal(playeScores)
if jerr == nil {
return string(b)
} else {
return "json error"
}
}
// get score
func (gm * GameManager) GetPlayerHighScore(gameName string, playerId string) (float64, error) {
return gm.rc.GetScore(gameName, playerId)
}
// store player new score for a week
func (gm * GameManager) StorePlayerScoreOnADay(gameName string, score float64, playerId string, numOfDaysOld int) {
gm.rc.AddToSet(gameName+util.GetDate(numOfDaysOld), score, playerId)
}
func (gm * GameManager) DeletePlayerScoreOnADay(gameName string, playerId string, numOfDaysOld int) (bool){
gameName = gameName+util.GetDate(numOfDaysOld)
redisRet, redisErr := gm.rc.RemScore(gameName, playerId)
if redisErr != nil && redisErr.Error() != REDIS_NIL {
go log.Printf("Error:DeletePlayerScoreOnADay: SUCESS gameName:%s, playerId:%s, redisErr:%v", gameName, playerId, redisErr)
return false
} else {
go log.Printf("Info :DeletePlayerScoreOnADay: SUCESS gameName:%s, playerId:%s, redisRet:%d", gameName, playerId, redisRet)
}
return true
}
// store player new score for a week
func (gm * GameManager) StorePlayerScoreDaily(gameName string, score float64, playerId string) {
gm.rc.AddToSet(gameName+util.CurrentDate(), score, playerId)
}
// store player new score for a week
func (gm * GameManager) GetPlayerScoreOnDay(gameName string, playerId string, numOfDaysOld int) (float64, error) {
return gm.GetPlayerHighScore(gameName+util.GetDate(numOfDaysOld), playerId)
}
// get top weekly 1000
func (gm * GameManager) GetTopPlayersOnDay(gameName string, topCount int64, numOfDaysOld int) (string) {
if numOfDaysOld > 6 {
return "";
}
playeScores := make([]dt.PlayerScore, 0)
dateGameName := gameName+util.GetDate(numOfDaysOld)
topResults, err := gm.rc.GetTop(dateGameName, topCount)
if err != nil && err.Error() != REDIS_NIL{
go log.Printf("Error:GetTopPlayersOnDay: dateGameName %s", dateGameName)
return "json error"
}
topResultsVal := reflect.ValueOf(topResults)
resultCount := topResultsVal.Len()
for i:=0; i<resultCount; i++ {
_score,_ := topResultsVal.Index(i).Field(0).Interface().(float64)
_pid,_ := topResultsVal.Index(i).Field(1).Interface().(string)
_name,_ := gm.GetPlayerName(gameName, _pid)
playeScores = append(playeScores, dt.PlayerScore{_name, _score})
}
sort.Sort(dt.ByScoreRev(playeScores))
if topCount < int64(len(playeScores)) {
playeScores = playeScores[:topCount]
}
go log.Printf("Info: GetTopPlayersOnDay: top %d playeScores: %v", topCount, playeScores)
b, jerr := json.Marshal(playeScores)
if jerr == nil {
return string(b)
} else {
return "json error"
}
}
func (gm * GameManager) GetTopPlayersThisWeek(gameName string, topCount int64) (string) {
playeScores := make([]dt.PlayerScore, 0)
donePersons := make(map[string] bool)
for i:=6; i>=0; i--{
topResults, err := gm.rc.GetTop(gameName+util.GetDate(i), topCount)
if err != nil && err.Error() != REDIS_NIL{
go log.Printf("Error:GetTopPlayersThisWeek: Game %s", gameName)
continue
}
topResultsVal := reflect.ValueOf(topResults)
resultCount := topResultsVal.Len()
for i:=0; i<resultCount; i++ {
_score,_ := topResultsVal.Index(i).Field(0).Interface().(float64)
_pid,_ := topResultsVal.Index(i).Field(1).Interface().(string)
_name,_ := gm.GetPlayerName(gameName, _pid)
playeScores = append(playeScores, dt.PlayerScore{_name, _score})
}
}
sort.Sort(dt.ByScoreRev(playeScores))
for i:=0; i<len(playeScores) && i<int(topCount); i++ {
if _,ok := donePersons[playeScores[i].N]; !ok {
donePersons[playeScores[i].N] = true
} else {
playeScores = append(playeScores[:i], playeScores[i+1:]...) // perfectly fine if i is the last element https://play.golang.org/p/hcUEguyiTC
i--
}
}
if topCount < int64(len(playeScores)) {
playeScores = playeScores[:topCount]
}
go log.Printf("Info: GetTopPlayersThisWeek: top %d playeScores: %v", topCount, playeScores)
b, jerr := json.Marshal(playeScores)
if jerr == nil {
return string(b)
} else {
return "json error"
}
}
// get rank among friends
func (gm * GameManager) GetScoreOfFriends(gameName string, playerId string, friendIds []string) (string) {
var topPlayersWithScores dt.ResponseData
playerScore, err := gm.GetPlayerHighScore(gameName, playerId)
if err != nil && err.Error() != REDIS_NIL {
go log.Printf("Error:GetScoreOfFriends: Error: %v", err)
return ""
}
totalCount := len(friendIds) + 1
topPlayersWithScores.PlayerIds = make([]string, totalCount)
topPlayersWithScores.Scores = make([]float64, totalCount)
for i:=1; i<totalCount; i++ {
topPlayersWithScores.PlayerIds[i] = friendIds[i-1]
topPlayersWithScores.Scores[i] = -1
score, err1 := gm.GetPlayerHighScore(gameName, friendIds[i-1])
if err1 != nil && err1.Error() != REDIS_NIL {
go log.Printf("Error:GetScoreOfFriends: Game %s, %v", gameName, err1)
} else {
topPlayersWithScores.Scores[i] = score
}
}
topPlayersWithScores.PlayerIds[0] = playerId
topPlayersWithScores.Scores[0] =playerScore
b, jerr := json.Marshal(topPlayersWithScores)
if jerr == nil {
return string(b)
} else {
return "json error"
}
}
Here is the test code:
package gamedatamanager_test
import (
dt "gogameserver/datatypes"
gdm "gogameserver/gamedatamanager"
util "gogameserver/util"
"testing"
"math/rand"
"time"
"strconv"
"encoding/json"
"sort"
)
const GAMENAME string = "00dummygame"
const PLAYERID string = "00playerid"
var PLAYERIDS = [] string{"00NeverAddThispid0", "00NeverAddThispid1", "00NeverAddThispid3", "00NeverAddThispid4", "00NeverAddThispid5"}
func TestStorePlayerData(t *testing.T) {
gm := gdm.New()
playerData := dt.NewWithId(PLAYERID)
done := gm.StorePlayerData(GAMENAME, playerData)
if !done {
t.Errorf("Data shall have been stored\n")
}
gm.DelPlayerData(GAMENAME, PLAYERID)
}
func TestGetPlayerData(t *testing.T) {
gm := gdm.New()
playerData := dt.NewWithId(PLAYERID)
done := gm.StorePlayerData(GAMENAME, playerData)
if !done {
t.Errorf("Error: Data shall have been stored\n")
}
playerDataStr := dt.Str(playerData)
playerDataStrFromDB, found := gm.GetPlayerData(GAMENAME, PLAYERID)
if !found {
t.Errorf("Error: GetPlayerData: Data shall have been found\n")
} else {
if playerDataStrFromDB != playerDataStr {
t.Errorf("Error: playerDataStr from redis: %s\n\tplayerDataStr shall have been %s\n", playerDataStrFromDB, playerDataStr)
}
}
gm.DelPlayerData(GAMENAME, PLAYERID)
}
func TestStorePlayerScore(t *testing.T) {
gm := gdm.New()
playerData := dt.NewWithId(PLAYERID)
playerData.A = 10.0
newScore := 10000.0
gm.DeletePlayerScore(GAMENAME, PLAYERID)
gm.StorePlayerData(GAMENAME, playerData)
success := gm.StorePlayerScore(GAMENAME, newScore, PLAYERID)
if !success {
t.Errorf("Error: StorePlayerScore: Data shall have been found\n")
} else {
newHiScore, err := gm.GetPlayerHighScore(GAMENAME, PLAYERID)
if err != nil {
t.Errorf("TestStorePlayerScore Error: GetPlayerHighScore, Error: %v\n", err)
} else {
if newHiScore != newScore {
t.Errorf("TestStorePlayerScore Error: newHiScore=%d. Added newScore=%d\n", newHiScore, newScore)
}
playerDataStrFromDB, success := gm.GetPlayerData(GAMENAME, PLAYERID)
if !success {
t.Errorf("TestStorePlayerScore Error: GetPlayerData\n")
} else {
playerData.A = newScore
playerDataStr := dt.Str(playerData)
if playerDataStrFromDB != playerDataStr {
t.Errorf("TestStorePlayerScore Error: playerDataStr from redis: %s\n\tplayerDataStr shall have been %s\n", playerDataStrFromDB, playerDataStr)
}
}
}
}
gm.DelPlayerData(GAMENAME, PLAYERID)
gm.DeletePlayerScore(GAMENAME, PLAYERID)
}
func TestGetPlayerRank(t *testing.T) {
gm := gdm.New()
scores := []float64{2,1,7,4, 3}
for i:=0; i<5; i++ {
gm.DeletePlayerScore(GAMENAME, PLAYERIDS[i])
playerData := dt.NewWithId(PLAYERIDS[i])
playerData.A = scores[i]
gm.StorePlayerData(GAMENAME, playerData)
gm.StorePlayerScore(GAMENAME, scores[i], PLAYERIDS[i])
}
rank := gm.GetPlayerRank(GAMENAME, PLAYERIDS[2])
if rank != 1{
t.Errorf("TestGetPlayerRank Error: Player rank should have been 1 but is : %d\n", rank)
}
for i:=0; i<5; i++ {
gm.DelPlayerData(GAMENAME, PLAYERIDS[i])
gm.DeletePlayerScore(GAMENAME, PLAYERIDS[i])
}
}
func TestGetScoreOfFriends(t *testing.T) {
gm := gdm.New()
scores := []float64{2,1,7,4, 3}
for i:=0; i<5; i++ {
gm.DeletePlayerScore(GAMENAME, PLAYERIDS[i])
playerData := dt.NewWithId(PLAYERIDS[i])
playerData.A = scores[i]
gm.StorePlayerData(GAMENAME, playerData)
gm.StorePlayerScore(GAMENAME, scores[i], PLAYERIDS[i])
}
scoresOfFriends := gm.GetScoreOfFriends(GAMENAME, PLAYERIDS[0], PLAYERIDS[2:5])
scoresOfFriendsExpectedStr := "{\"PlayerIds\":[\"00NeverAddThispid0\",\"00NeverAddThispid3\",\"00NeverAddThispid4\",\"00NeverAddThispid5\"],\"Scores\":[2,7,4,3]}"
if scoresOfFriends != scoresOfFriendsExpectedStr{
t.Errorf("TestGetScoreOfFriends Error: scoresOfFriends str is : %s\n", scoresOfFriends)
}
for i:=0; i<5; i++ {
gm.DelPlayerData(GAMENAME, PLAYERIDS[i])
gm.DeletePlayerScore(GAMENAME, PLAYERIDS[i])
}
}
func TestGetTopPlayers(t *testing.T) {
gm := gdm.New()
scores := []float64{2,1,7,4, 3}
names := make([] string, 5)
playeScores := make([]dt.PlayerScore, 0)
gm.DelKey(GAMENAME)
for i:=0; i<5; i++ {
names[i] = util.RandStringRunes(5)
gm.DeletePlayerScore(GAMENAME, PLAYERIDS[i])
playerData := dt.NewWithId(PLAYERIDS[i])
playerData.A = scores[i]
playerData.N = names[i]
gm.StorePlayerData(GAMENAME, playerData)
gm.StorePlayerScore(GAMENAME, scores[i], PLAYERIDS[i])
playeScores = append(playeScores, dt.PlayerScore{names[i], scores[i]})
}
sort.Sort(dt.ByScoreRev(playeScores))
topCount := 3
top3 := gm.GetTopPlayers(GAMENAME, int64(topCount) )
var top3Scorers []dt.PlayerScore
json.Unmarshal([]byte(top3), &top3Scorers)
for i:=0; i<topCount; i++{
if playeScores[i].N != top3Scorers[i].N || playeScores[i].S != top3Scorers[i].S {
t.Errorf("Error: TestGetTopPlayers, playeScores[i] : %v\n, top3Scorers[i]: %v\n", playeScores[i], top3Scorers[i])
}
}
for i:=0; i<5; i++ {
gm.DelPlayerData(GAMENAME, PLAYERIDS[i])
gm.DeletePlayerScore(GAMENAME, PLAYERIDS[i])
}
}
func TestGetTopPlayersThisWeek(t *testing.T) {
gm := gdm.New()
r := rand.New(rand.NewSource(time.Now().UnixNano()))
scores := make([] float64, 0)
names := make([] string, 10)
id := make([] string, 10)
for i:=0; i<10; i++ {
names[i] = util.RandStringRunes(5)
id[i] = strconv.Itoa(i)
playerData := dt.NewWithId(id[i])
playerData.N = names[i]
gm.StorePlayerData(GAMENAME, playerData)
}
playerMaxScore := make(map[string] float64)
for i:=0; i<10; i++ {
playerMaxScore[names[i]] = 0
}
// for 7 days
for d:=0; d<7; d++ {
gm.DelKey(GAMENAME+util.GetDate(d))
for i:=0; i<10; i++ {
score := r.Float64()*100
scores = append(scores, score)
if playerMaxScore[names[i]] < score {
playerMaxScore[names[i]] = score
}
gm.StorePlayerScoreOnADay(GAMENAME, score, id[i], d)
}
}
playeScores := make([]dt.PlayerScore, 0)
for k,v := range playerMaxScore {
playeScores = append(playeScores, dt.PlayerScore{k, v})
}
sort.Sort(dt.ByScoreRev(playeScores))
topCount := 5
topWeeklyScorersJson := gm.GetTopPlayersThisWeek(GAMENAME, int64(topCount) )
var topWeeklyScorers []dt.PlayerScore
json.Unmarshal([]byte(topWeeklyScorersJson), &topWeeklyScorers)
for i:=0; i<topCount; i++{
if playeScores[i].N != topWeeklyScorers[i].N || playeScores[i].S != topWeeklyScorers[i].S {
t.Errorf("Error: TestGetTopPlayersThisWeek, playeScores[i] : %v\n, topWeeklyScorers[i]: %v\n", playeScores[i], topWeeklyScorers[i])
}
}
for i:=0; i<10; i++ {
gm.DelPlayerData(GAMENAME, id[i])
}
for d:=0; d<7; d++ {
for i:=0; i<10; i++ {
gm.DeletePlayerScoreOnADay(GAMENAME , id[i], d)
}
}
}
Here is the complete GoGameServer: https://github.com/ediston/gogameserver
Please review it and tell me how can I write better code.