Assign points by rank to players of multiple games - php

I'm building a multi-game tournament scoreboard, where many players can play many games, many times. I want to assign points to players per game based on their rank for each game, not their actual score.
e.g.
Donkey Kong
Rank | Player | Score | Points Awarded
1 | Player2 | 34,000 | 1,000
2 | Player1 | 32,000 | 999
3 | Player3 | 29,000 | 998
Robotron
Rank | Player | Score | Points Awarded
1 | Player1 | 39,000 | 1,000
2 | Player3 | 32,000 | 999
3 | Player2 | 21,000 | 998
Tournament Standings
Player1 - 1,999 Points
Player2 - 1,998 Points
Player3 - 1,997 Points
So far I have ranking and points calculations working just fine...
SELECT
`id`,
`userID`,
`gameID`,
`gamescore`,
`rank`,
1001.0 - (rank) AS points
FROM (
SELECT
`id`,
`userID`,
`gameID`,
`gamescore`,
#curr_rank := IF(#prev_rank = id, #curr_rank, #curr_rank + 1) AS rank,
#prev_rank := id
FROM
`submit_score`,
(SELECT #curr_rank := 0) y,
(SELECT #prev_rank := NULL) z
WHERE `submit_score`.`tournID` = 2
ORDER BY `gamescore` DESC
) ranked_game;
But I need to be able to assign the points by rank-per-game and then have a grand total of points for each player that I can then show in a list.

Your DB should to look something like
Players
ID | Player Nickname
1 | Player1
2 | Player2
3 | Player3
Donkey Kong
PlayerID | Score
Player2ID | 34,000
Player1ID | 32,000
Player3ID | 29,000
Robotron
PlayerID | Score
Player1ID | 39,000
Player3ID | 32,000
Player2ID | 21,000
DB should be only used to store data. Rank and awarded points should be moved to the code as those are redundant parts.
Then query each table with desc sort on score columns. That will create ranked tables with top scorer as #1 etc. That retrieved data store in arrays for each game with Key->Values where Key will be a rank # (1+ incremental) and Values can be other array with stored players data (after join) and a score or a string something like PlayerX | Score# if you need a score data for anything because for real you need only Players data sort on scores per game.
After that you need to loop through player table and create an array of players where you will store tournament points retrieved from looping each game array and decrease tournament points on each rank and assign to a proper player.
Hope it helps
PS.
For existing data I will create views something like
set #max_points=1000;
select Rank, Player,
#current_points := IF(Rank = 1, #max_points, #current_points-1) AS points
from DonkeyKong
for each game
then do final view to sum all tournaments points
select dk.Player AS Player,(dk.Points + ro.Points) AS Total
from RO_view AS ro
left join DK_view AS dk
on dk.Player = ro.Player

Related

PHP/MySQL Rank by multiple columns

I'm developing a game that scores players in 2 different ways, A, B.
In this game I have a ranking page that shows what position you are in ranking A, B and A+B as well as a list of players ordered by A+B.
What would be the most efficient way to retrieve your rank in these different scoring situations as well as the nearby users for rank A+B (to display on the list)?
I assume I would have to do a pass over every user at least once. Should I attempt this with multiple left joins and subselects and count users with score A/B/A+B greater than yours, or just query for the whole user+score list and calculate the ranks with a PHP function?
EXAMPLE:
UID | A | B
----------------
1 | 100 | 50
2 | 150 | 20
3 | 10 | 100
Assuming the user viewing the ranking is UID=2 we should see:
SCORE LIST (A+B):
2 - 170
1 - 150
3 - 110
You are #1 in Score A.
You are #3 in Score B.
You are #1 in Score A+B.
create table my_table
(UID int not null auto_increment primary key
,A int not null
,B int not null
);
insert into my_table values
(1,100,50),
(2,150,20),
(3,10,100);
SELECT uid
, FIND_IN_SET(a,a_score) a_rank
, FIND_IN_SET(b,b_score) b_rank
, FIND_IN_SET(a+b,ab_score)ab_rank
FROM my_table
, ( SELECT GROUP_CONCAT(DISTINCT a ORDER BY a DESC) a_score
, GROUP_CONCAT(DISTINCT b ORDER BY b DESC) b_score
, GROUP_CONCAT(DISTINCT a+b ORDER BY a+b DESC) ab_score
FROM my_table
) n
[WHERE uid=2];
+-----+--------+--------+---------+
| uid | a_rank | b_rank | ab_rank |
+-----+--------+--------+---------+
| 1 | 2 | 2 | 2 |
| 2 | 1 | 3 | 1 |
| 3 | 3 | 1 | 3 |
+-----+--------+--------+---------+
http://sqlfiddle.com/#!9/97da6/13

Calculate quantiles for a score/ranking system (PHP / MySQL)

There are two tables:
Table user:
+----+-----------+
| id | user_name |
+----+-----------+
| 1 | Alice |
| 2 | Steve |
| 3 | Tommy |
+----+-----------+
Table result:
+----+---------+-------+-------------+
| id | user_id | score | timestamp |
+----+---------+-------+-------------+
| 1 | 1 | 22 | 1410793838 |
| 2 | 1 | 16 | 1410793911 |
| 3 | 2 | 9 | 1410793920 |
| 4 | 1 | 27 | 1410794007 |
| 5 | 3 | 32 | 1410794023 |
+----+---------+-------+-------------+
What I have so far is a "top 3", which works great and looks like this:
SELECT MAX(m.score) AS score, u.user_name
FROM result AS r
INNER JOIN user AS u ON r.user_id = u.id
GROUP BY r.user_id
ORDER BY r.score DESC
LIMIT 3;
+-------+-----------+
| score | user_name |
+-------+-----------+
| 32 | Tommy |
| 27 | Alice |
| 9 | Steve |
+-------+-----------+
The table is actually filled with hundreds of results, this is just an example. I'm looking for a compact algorithm to get the rank of a specific user in relation to all other users in %. The goal is to output something like "you are in the top 5%/10%/20%/50%" or "you are below average". While it's easy to determine if someone is below average (score < AVG(score)), I have no clue how to determine the other ranks.
If I got all correctly, it's just relative maximum calculation:
SELECT
user_name,
MAX(score) AS max_score,
CASE
WHEN ROUND(100*MAX(score)/maximum, 2)>=95 THEN 'In top 5%'
WHEN ROUND(100*MAX(score)/maximum, 2)>=90 THEN 'In top 10%'
WHEN ROUND(100*MAX(score)/maximum, 2)>=75 THEN 'In top 25%'
WHEN ROUND(100*MAX(score)/maximum, 2)>=50 THEN 'In top 50%'
WHEN ROUND(100*MAX(score)/maximum, 2)>=0 THEN 'Below average'
END AS score_mark
FROM
`result`
INNER JOIN `user`
ON `result`.user_id=`user`.id
CROSS JOIN
(SELECT MAX(score) AS maximum FROM `result`) AS init
GROUP BY
user_id
So, counting from maximum score per all table and grouping it for specific user. Check the fiddle.
As mentioned below, this counting method involves simple way to determine average (i.e. all it's based on total maximum). This may be not the thing which is needed. By that I mean, that if question is about calculation relative position according to other scores (not maximum) - then it's more complicated:
SELECT
maxs.*,
#num:=#num+1 AS order_num,
CASE
WHEN 100*(#num-1)/(user_count-1) <= 5 THEN 'In top 5%'
WHEN 100*(#num-1)/(user_count-1) <= 10 THEN 'In top 10%'
WHEN 100*(#num-1)/(user_count-1) <= 25 THEN 'In top 25%'
WHEN 100*(#num-1)/(user_count-1) <= 50 THEN 'In top 50%'
WHEN 100*(#num-1)/(user_count-1) <= 100 THEN 'Below average'
END AS score_mark
FROM
(SELECT
user_name,
MAX(score) AS max_score
FROM
`result`
INNER JOIN `user`
ON `result`.user_id = `user`.id
GROUP BY
user_id
ORDER BY
max_score DESC) AS maxs
CROSS JOIN
(SELECT
#num:=0,
COUNT(DISTINCT user_id) AS user_count
FROM
`result`) AS init
-since now we must first re-count our positions and later build relative calculations over that. Here is the corresponding fiddle. Here, however, I'm applying linear formula to count 1-st position as "zero" and last position as "100". If that's not an intention (there will be edge-cases, like "3" in "50%" for "5 total" in fiddle) - then you may change divisor to user_count
Here is another version
SELECT user_name, score,(CASE
WHEN score BETWEEN #max-((#max-#min)/10) AND #max THEN '10'
WHEN score BETWEEN #max-((#max-#min)/5) AND #max THEN '20'
WHEN score BETWEEN #max-((#max-#min)/2) AND #max THEN '50'
ELSE 'more50'
END) as rangescore,
user_name
FROM result r
INNER JOIN user u ON r.user_id = u.id,
(SELECT #max := MAX(score) FROM result)x,
(SELECT #min := MIN(score) FROM result)y
ORDER BY score DESC
You can use AVG(score) instead of MAX f you want to compare the average score of the user.
Or remove the aggregate functions and GROUP BY if you want every score.
FIDDLE
FIDDLE GROUP
Ok, now with your new declarations, I will provide you another solution:
As you said you can use PHP and MySQL togheter, I provide you a combined one.
You want to compute your quantiles (to have an idea what this is quantiles on wikipedia), because if you have a top scorer with about 10000 points and all other players just have 100 points and below, the ones with 100 points are in the top 5% as player, but there score is under 50% of the top scorer.
With this in mind, we can count the number of players, get the score at the wanted percent of players and compare, were the players score fits in.
First select all the maximas, minimas, counter etc.
SELECT
COUNT(`result`.`score`) `count`,
MAX(`result`.`score`) `max`,
MIN(`result`.`score`) `min`,
AVG(`result`.`score`) `avg`
FROM
`result`
GROUP BY
`result`.`user_id`
ORDER BY
`result`.`score` DESC
After you have the complete data, you can compute your quantiles.
SELECT
`result`.`score`
FROM
`result`
GROUP BY
`result`.`user_id`
ORDER BY
`result`.`score` DESC
LIMIT FLOOR($count*$percent), 1
//where $count is the value from the first query and $percent is the wanted quantile e.g. 5%
After that you know the value for your quantile and you can compare the actual value with the ones here.
//where $percentNN is the score from the previous query
if($score > $percent50) echo "top 50%";
if($score > $percent20) echo "top 20%";
if($score > $percent10) echo "top 10%";
if($score > $percent5) echo "top 5%";
maybe, we can combine the multiple queries to one.

Counting in a complex query

I will explain my problem with an example:
Tables:
place - comment - activities_comment - activities
User can comment a place and select what kind of activities are being able to do in that place.
Activities are for example: run, swim, walk and climb.
So some users would vote more than 1 activity, so i can't put it right in the same table COMMENT, i need to create a pivote table ACTIVITIES_COMMENT, the problem goes here, i was able to show all the comments with their activities selected in each place..
But now i want to count the number of activities and order them by the most selected activity to the less selected by the users in the comments for each place.
How can I do that??
The best thing i can do is:
$place = Place::with('city','comments')
->where('id',$placeId)->first();
$place->activities = Activity::join('activities_comment', 'activity.id', '=', 'activities_comment.activity_id')
->join('comment', 'comment.id', '=', 'activities_comment.comment_id')
->select('comment.id','activities_comment.activity_id',
DB::raw('(SELECT count(activities_comment.activity_id) FROM activities_comment WHERE activities_comment.activity_id = 1) as run'),
DB::raw('(SELECT count(activities_comment.activity_id) FROM activities_comment WHERE activities_comment.activity_id = 2) as swim'),
DB::raw('(SELECT count(activities_comment.activity_id) FROM activities_comment WHERE activities_comment.activity_id = 3) as walk'),
DB::raw('(SELECT count(activities_comment.activity_id) FROM activities_comment WHERE activities_comment.activity_id = 4) as climb'),
)
->where('comment.place_id',$place->id)
->get();
The problem is that this query counts the most selected activities but in ALL places, i want to count only in each place.
EDIT:
Example rows:
place table:
id | name
----------
1 | Alaska
2 | Peru
3 | Argentina
comment table:
id | user_id | place_id | text
------------------------------------
1 | 1 | 1 | some text
2 | 3 | 1 | some text
3 | 2 | 2 | some text
activity table:
id | name
----------
1 | run
2 | swim
3 | walk
4 | climb
activity_comment table:
id | comment_id | activity_id
------------------------------
1 | 1 | 1
2 | 1 | 2
3 | 2 | 2
4 | 3 | 2
When i get into Alaska comments, i would like to see the times that users selected an activity there, for alaska it will show:
run: 1 time
swim: 2 times
walk: 0 times
climb: 0 times
If i go to Peru comments:
run: 0 times
swim: 1 time
walk: 0 times
climb: 0 times
Thank you in advance.
First, get the tables all linked up via the joins showing how each table is related to the next by their respective keys. Then, its just a matter of each name (city/location), and name of the activity and getting a count grouped by the respective location and activity.
The order by clause will put all same locations first, then within each location will put the highest count activity secondary... if any have same counts, then the names will be alpha ordered
SELECT
p.name,
a.name as Activity,
COUNT(*) as NumComments
from
place p
join comment c
ON p.id = c.place_id
join activity_comment ac
ON c.id = ac.comment_id
join activity a
ON ac.activity_id = a.id
group by
p.name,
a.name
order by
p.name,
COUNT(*) desc,
a.name
Now, you will just need to plug in your WHERE clause (before the group by) for whatever location or activity you may be interested in.

MYSQL - select all time high score

Assuming we have a table like below
id | name | userid | score | datestamp |
------------------------------------------------------------
1 | john | 1 | 44 | 2013-06-10 14:25:55
2 | mary | 2 | 59 | 2013-06-10 09:25:51
3 | john | 1 | 38 | 2013-06-10 21:25:15
4 | elvis | 3 | 19 | 2013-06-10 07:25:18
5 | john | 1 | 100 | 2013-06-14 07:25:18
I want to select the all time high-score of each user.
so for example if the player john have played ten rounds in 2013-06-10 and his score is 430 in total for that day. and in 2013-06-14 he plays 16 rounds and his scores is 1220 in total for that day. i want to display the the best score of user john, and so on for the others players. in the example above johns best score was 2013-06-14 with score 1220.
Detailed example:
If user John plays 3 rounds in 2013-06-10. First round he scores 44, second time he scores 38 and third time he scores 55. His total score for that day is 137. And on the next day 2013-06-11 he plays 5 rounds with a total score of 220. In this example his best total score so far is 220. So it should group all scores for each day to a total. And then compare this total with other days total and display the highest total of that user
Thanks in advance
This should do the trick:
SELECT userId, name, MAX(score) as highScore
FROM (
SELECT userID, name, SUM(score) as score, date(datestamp) as scoreDate
FROM TableName
GROUP BY userID, scoreDate
) dailyScores
GROUP BY userId
The inner query fetches the totals of each user's scores on each date (timestamp converted to date to remove time information), the outer query then gets the highest total score of each date for each user.
I took the liberty of basing on J W's fiddle, and I added another row to your example data so the functionality is obvious, try it out here:
http://www.sqlfiddle.com/#!2/f6bea/3
SELECT a.*
FROM Tablename a
INNER JOIN
(
SELECT userID, MAX(score) max_Score
FROM TableName
GROUP BY userID
) b ON a.userID = b.userID AND
a.score = b.max_Score
SQLFiddle Demo
For faster searching, you need to have compound index on (userid, score). Here's how to implement it:
ALTER TABLE TableName ADD INDEX (userid, score)

How best to get someone's 'rank' from a scores table with php and mysql without looping

My table will hold scores and initials.
But the table wont be ordered.
I can get the total row count easy enough and I know I can get all of them and Order By and then loop through them and get the rank THAT way...
But is there a better way? Could this maybe be done with the SQL statement?
I'm not TOO concerned about performance so if the SQL statement is some crazy thing, then Ill just loop.
Sorry - Table has id as primary key, a string to verify unique app install, a column for initials and a column for score.
When someone clicks GET RANK... I want to be able to tell them that their score is 100 out of 1000 players.
SELECT s1.initials, (
SELECT COUNT(*)
FROM scores AS s2
WHERE s2.score > s1.score
)+1 AS rank
FROM scores AS s1
Do you have a primary key for the table or are you fetching data by the initials or are you just fetching all the data and looping through it to get the single record? How many results are you trying to fetch?
If you only want to fetch one record, as the title suggests, you would query the database using a WHERE conditional:
SELECT score FROM table WHERE user_id = '1';
See this answer: https://stackoverflow.com/a/8684441/125816
In short, you can do something like this:
SELECT id, (#next_rank := IF(#score <> score, 1, 0)) nr,
(#score := score) score, (#r := IF(#next_rank = 1, #r + 1, #r)) rank
FROM rank, (SELECT #r := 0) dummy1
ORDER BY score DESC;
And it will produce a result like this:
+------+----+-------+------+
| id | nr | score | rank |
+------+----+-------+------+
| 2 | 1 | 23 | 1 |
| 4 | 1 | 17 | 2 |
| 1 | 0 | 17 | 2 |
| 5 | 1 | 10 | 3 |
| 3 | 1 | 2 | 4 |
+------+----+-------+------+
Note that users with equal scores have equal ranks. :-)

Categories