This is a high level/design type question rather than anything specific.
In this game http://koreanwordgame.com/ users have to get as many correct answers as possible in a minute (though its set to 5 seconds for development purposes). As soon as the minute is up I fire an Ajax call with JQuery passing the users score.
On the server side I use this SQL query to get the 5 scores above and 5 below
$getquery = "(SELECT *
FROM highscore
WHERE score >= '$score'
ORDER BY score ASC
LIMIT 5)
UNION ALL
(SELECT *
FROM highscore
WHERE score < '$score'
ORDER BY score DESC
LIMIT 5)";
This returns an HTML table to the client which you will see upon completing the (5 sec) challenge (you don't have to get any answers right).
Now, ultimately I want the table to be about 100 rows long (although it will still only display the surrounding 10 cells, 5 above, 5 below), with the rows position (ID) determined not chronological as it is now but by the score, so that if someone gets 1000 answers right they would go straight into position 1 on the database. Obviously if a user gets lower than any of the 100 rows already existing in the table then it should simply be discarded. Then when the table comes back I want a text input to appear inside the table cell where the user is placed so they can enter their name and fire off another Ajax call so their entry gets inserted into the right place in the database.
I know it would be easier just to ask the user to enter their details first and then scoop off the top 10 results whether they got there or not, but the idea is that I want anyone to be able to get some kind of entry as this will encourage people to play more.
A tough one perhaps..I certainly haven't been able to find anything similar by Googling, all help much appreciated!
EDIT:
In case anyone is interested this is what I ended up with, it's far from finished I need to make it way more robust for example entering an anonymous username in case the user closes the browser and also prevent double posts (though this will be client side).
but thank you everyone for the help, I would not have done it without you. If you see any obvious improvements that could be made feel free to point them out!
<?php
$dbcon = mysql_connect("localhost", "XXXX", "XXXX") or die(mysql_error());
mysql_select_db("tulesblo_koreangame", $dbcon) or die(mysql_error());
mysql_query("SET NAMES utf8");
$name = $_POST["name"];
$email = $_POST["email"];
$score = $_POST["score"];
$table = "";
$submit = "";
$input = "";
$newposition = $_POST['position'];
$position = mysql_query("(SELECT position
FROM highscore
WHERE score < '$score'
ORDER BY score DESC
LIMIT 1)");
if(!$name){
$gethigherrows = "(SELECT *
FROM highscore
WHERE score >= '$score'
ORDER BY score ASC
LIMIT 5)";
$getlowerrows = "(SELECT *
FROM highscore
WHERE score < '$score'
ORDER BY score DESC
LIMIT 5)";
$higherrows= mysql_query($gethigherrows);
$lowerrows= mysql_query($getlowerrows);
if(mysql_error())echo mysql_error();
while($row=mysql_fetch_array($higherrows))
{
$uppertable .= "<tr><td>$row[position]</td><td>$row[name]</td> <td>$row[score]</td></tr>";
}
$x = 0;
if (mysql_num_rows($lowerrows) > 0)
{ mysql_query("UPDATE highscore SET position = position + 1 WHERE score < '$score'")or die("update failed");
while($row=mysql_fetch_array($lowerrows))
{
if ($x == 0)
{$position = $row['position'];};
$x++;
$newpos = $row[position]+1;
$lowertable.= "<tr><td>$newpos</td><td>$row[name]</td> <td>$row[score]</td></tr>";
}
$input = "<tr><td id='position'>$position</td><td><input id='nameinput'type='text' /></td><td>$score</td></tr>";
$submit = "<br />Enter email if you want to receive a prize!<br /><input id='emailinput'type='text' /><br /><input id='submithighscore'type='submit' value='Submit'>";
}
$table .= "<table id='scoretable'><tr><th>Position</th><th>Name</th><th>Score</th></tr>";
$table .= $uppertable;
$table .= $input;
$table .= $lowertable;
$table .= "</table>";
$table .= $submit;
$table .= "<br /><span class='msg'></span>";
echo $table;
}else{ echo($newposition);
mysql_query("INSERT INTO highscore VALUES (NULL, '$score', '$name', '$email', '$newposition')");
}
?>
You did not say where lies your problem, but I suppose it's mostly a misconception about SQL. Here's how I would do the whole app.
For the SQL schema:
You shouldn't care about the order of the rows in the DB. You can set the order in the queries (SELECT, etc). There is no "right place" to insert.
You don't need a "rank" column, for the same reason.
Your table can be as simple as: id (INT AUTO_INCREMENT), name, score, when (timestamp).
Initialize it with 100 dummy scores.
For the workflow:
Do not care for performance now, aim for readability.
As you wrote, once a player has finished, use AJAX to find the 10 surrounding scores. You already have the SQL queries for this part. You can either prepare the HTML on the server side in PHP, or send raw data in order build the HTML with Javascript. This can be done in one request (use json_encode() in PHP to return an array/object).
If there are no scores below, then it's not one of the 100 best scores, do nothing.
Else, ask the user its name, and send a new AJAX request. The SQL can be kept simple:
INSERT INTO Highscore SET name=?, score=?, when=NOW()
After each insertion, delete the lowest score
DELETE FROM Highscore ORDER BY score ASC, when DESC LIMIT 1
If you want to prevent cheating, you'll need to add hashes in your ajax calls. For instance, send (username, score, hash=md5(username.score."secretphrase") and check the hash still matches the username and score.
If you want to display the ranks along with the scores, then put a "rank" colum in your table. When you insert a score, set its rank to the max rank of the lower scores (you already computed this for the first AJAX request, so send it in the second AJAX request). Then UPDATE Highscore SET rank = rank + 1 WHERE score < ?.
I'm havinga pickle to understand what you're exactly after. Here's a query to insert the result in the highscore.
$str_user_id = $_GET['user_id'];
$str_score = $_GET['score'];
mysql_query("
INSERT INTO highscore (position, user_id, score)
SELECT (#rownum := $rownum + 1) AS position, user_id, score
FROM (SELECT user_id, score
FROM (SELECT '{$str_user_id}' AS user_id, '{$str_score}' AS score
UNION ALL
SELECT user_id, score
FROM highscore) AS h, (#rownum := 0) AS vars
WHERE '{$str_score}' <= 100
ORDER BY score ASC) AS h2)
LIMIT 100
ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), score = VALUES(score)
");
I couldn't entirely make sense of your question, but based on this:
"I know it would be easier just to ask the user to enter their details first and then scoop off the top 10 results whether they got there or not, but the idea is that I want anyone to be able to get some kind of entry as this will encourage people to play more"
I suspect you want the score table shown to the user to look something like this:
1. John Doe (score: 1,000,000)
2. Larry Roe (score: 999,999)
3. Jane Doe (score: 985,742)
...
5746. Susan Player (score: 894)
5747. *** YOU *** (score: 893)
5748. Joe R. Player (score: 889)
I second Mytskine's initial suggestion that you shouldn't try to store the ranks in the DB at all — keeping a rank column up to date is possible, like Mytskine shows at the end of their answer, but it's a lot of work for little if any gain. Just have your score table consist of the obvious columns like name and score and time, plus maybe an AUTO_INCREMENT column id.
Now, let's say you want to show the N highest-scoring players, plus K players before and after the new score. First, we need to figure out the player's rank:
SELECT COUNT(*) + 1 FROM scores WHERE score > ?
Here, the ? is a placeholder for the player's score. This counts how many players have a higher score than the one we're going to insert below. If we favor the latest scores in the case of ties, this plus one will be the player's rank. Let's denote this rank by R.
Now, if R < N + K, we might was well just show the top N + 2*K (or max(N, R+K) or whatever) entries:
SELECT * FROM scores ORDER BY score DESC, time DESC LIMIT N+2*K
Otherwise, we need to do it in three steps: first, fetch the top N entries:
SELECT * FROM scores ORDER BY score DESC, time DESC LIMIT N
then, the K entries above the player:
SELECT * FROM scores WHERE score > ? ORDER BY score ASC, time ASC LIMIT K
and finally the K entries below the player:
SELECT * FROM scores WHERE score <= ? ORDER BY score DESC, time DESC LIMIT K
Note that the second result set will be in reverse order, but that's OK — just reverse it in PHP. Now we need to assign ranks to the entries we've fetched, but that's simple: the first set has ranks 1 to N, the second set (after reversal) has ranks R-K to R-1, and the last set has ranks R+1 to R+K. (Rank R belongs to the current player, of course.)
Finally, after the player has entered their name, we can insert the player's score into to the database simply with:
INSERT INTO scores (name, score, time) VALUES (?, ?, ?)
If you want, you can also remove the lowest scores from the database at this point to limit its size, but in practice it probably won't matter unless you have literally billions of players. (A single score entry is going to take a few dozen bytes, mostly depending on how long a name the player enters, so a million entries will still be just a few dozen megabytes.) You should, however, remember to create an index on (score, time) to keep the queries efficient.
Related
I'm building a game using a platform called Unity. The game integrates with some php scripts and a database I have. Players can register in the game and all info gets stored in the database, including their high score.
I'm trying to write php code that will query the database and complete the following action
*Sort the table by high score
*Return the "username" and "high score" values for the top 25 entries
I don't know how to construct a query to do this. Below is a screen cap of my table and a sample php script of a query used to log in
database of users and high scores
//php below, script checking that there is only one instance of the username
$namecheckquery = "SELECT username, totalpoints, highscore FROM players WHERE username='" . $username . "'";
$namecheck = mysqli_query($con, $namecheckquery) or die("2: Name check query failed"); //error code #2 - name check query failed
if (mysqli_num_rows($namecheck) != 1)
{
echo "5: Either no user with name or more than one"; //error code #5: number of names matching does not = 1
exit();
}
$existinginfo = mysqli_fetch_assoc($namecheck);
You need ORDER BY {column_name} {ASC|DESC} (ascending by default), and then LIMIT BY {offset}, {length}:
SELECT username, hightscore
FROM players
ORDER BY highscore DESC
LIMIT 0, 25
You can also add multiple columns to order by, so in case of equality the next column will be used, like:
SELECT username, hightscore
FROM players
ORDER BY highscore DESC, totalpoints DESC
LIMIT 0, 25
I am having trouble understanding how a loop I have created works.
I have input fields on another page, which where results are sent to the results table in my database, with team name, team score, opposition score, opposition name.
Now the page where the results are entered mirrors the current contents of the results table so that previously entered scores appear already in the correct input field, and the ones with no socres enetered are at 0.
This page works perfectly in that a score that is edited, replaces the score in the results table, blank ones are left blank etc.
Howwever, I need to use these results to update my league table table in my database.
Currently I have a rather large loop, which (after I have fetched team name, team score, opposition score, opposition name, from the results table), works out how many points to give that team, and the opposing team, plus how many to add to the 'win' column and the 'loss' column etc of my league table.
The problem I have is that it will only ever do one result per team, because on each iteration of the loop, as soon as it finds a matching if statement (ie if team_score>opposition score) it updates the table, before running through the other results to find that teams results.
Because an edited result needs to be treated as an edited result, not an additional result, this is the only way I have been able to find to get even close.
here is a snippet of the code. There are many more if statementswithin the for loop but they are not needed to describe the problem I am having.
$query = $database->query("SELECT team_name, team_score, opposition_score, opposition_name from results_a");
while ($row = $query->fetch(PDO::FETCH_NUM)) {
$team[] = ($row[0]);
$team_score[] = ($row[1]);
$opposition_score[] = ($row[2]);
$opposition[] = ($row[3]);
}
$count=count($team);
for ($i = 0; $i < $count; $i++) {
$team_bonus[$i] = $team_score[$i] / 2;
$opposition_bonus[] = $opposition_score[$i] / 2;
//TEAM WINS, NO OPPOSITION BONUS
else if (($team_score[$i] > $opposition_score[$i]) && ($team_bonus[$i] > $opposition_score[$i])) {
$team_points[]+=3;
$team_win[]+=1;
$team_draw[]+=0;
$team_loss[]+=0;
$team_extra[]+=0;
$opposition_points[]+=0;
$opp_win[]+=0;
$opp_draw[]+=0;
$opp_loss[]+=1;
$opp_extra[]+=0;
$team_played[]+=1;
$opposition_played[]+=1;
}
$team_gd[] = $team_score[$i] - $opposition_score[$i];
$opposition_gd[] = $opposition_score[$i] - $team_score[$i];
//UPDATE LEAGUE TABLE
$query = $database->query("UPDATE pool_a SET played='$team_played[$i]',
win='$team_win[$i]',
draw='$team_draw[$i]',
loss='$team_loss[$i]',
goals_for='$team_score[$i]',
goals_against='$opposition_score[$i]',
goal_difference='$team_gd[$i]',
bonus_points='$team_extra[$i]',
points='$team_points[$i]'
where team_name = '$team[$i]'");
$query2 = $database->query("UPDATE pool_a SET played='$opposition_played[$i]',
win='$opp_win[$i]',
draw='$opp_draw[$i]',
loss='$opp_loss[$i]',
goals_for='$opposition_score[$i]',
goals_against='$team_score[$i]',
goal_difference='$opposition_gd[$i]',
bonus_points='$opp_extra[$i]',
points='$opposition_points[$i]'
where team_name='$opposition[$i]'");
The+= within the if statement are useless, because instead of going to the next row where the team name is the first teams name, it runs the query, then when that teams name appears again, it replaces the data next time the query runs.
Very stuck, have spent a lot of time on this!
If you need any more info let me know.
Many thanks
I am working with a table that contains logs for users reported exercise distances. I want to rank users by the sum of their logged distance.
First I find all the User ID's:
select distinct wordpress_user_id from wp_exercise_log
Then I loop through that and get the name and sum(distance) with:
foreach($users as $user) {
// DISTANCE
$user_distance = floor($wpdb->get_var( "select sum(distance) from wp_exercise_log where wordpress_user_id='$user'"));
// FULL NAME
$user_info = get_userdata($user);
$full_name = $user_info-> user_firstname . ' ' . $user_info-> user_lastname;
}
Heres my problem: Unless I append them to a multidimensional array and sort with PHP, I'm not sure how I can rank the users.
Is there a better way to select all this data and sort it with one SQL query?
select
wordpress_user_id,
sum(distance) as user_distance
from
wp_exercise_log
group by
wordpress_user_id
order by
user_distance desc
Please mind ORDER BY GROUP_FUNCTION() is always causing perfomance issues.
You may consider to add some column user_total_distance to your users table and update it every change in wp_exercise_log.
I'm working on the photo section of my website, but more precisely on the next/previous link with sorting options.
There are two different option for sorting that exist, first there is a filter (Latest, Trending, Favorite, User etc.) and secondly a sorting option for some of the previous filters (date, views, ratings etc.)
I am having trouble finding a good solution for easily retrieving the next/previous picture for my slideshow.
This is the little bit of SQL I have for getting the next picture for a filter:latest
if ($filter == "latest") {
$sql = "SELECT *
FROM photos
WHERE status = 0 AND id < ".$id."
ORDER BY id DESC
LIMIT 1
";
$result = $db->sql_query($sql);
$sql2 ="SELECT *
FROM photos
WHERE id = (SELECT MAX(id) FROM photos WHERE status = '0')
ORDER BY id DESC
LIMIT 1
";
}
I am wondering if there is an easier way to implement this in my project ? maybe a php class exists to do something like this ?
I feel like I'm going to spend a long time getting all these SQL queries to work properly if I have to do it all like this.
Edit
This is the layout of my photos table
`id` -> unique ID, autoincremented
`title`
`img_name` -> contains the image file name
`date_created` -> when the picture was uploaded
`uploader` -> id of the user that uploaded
`views` -> number of views the picture has
`up_votes` -> votes used to calculate the Wilson score interval
`down_votes`
`net_votes` -> up vote minus down votes
There are other tables like one that links user with friends they have, it is called users_friends
`user_id` -> contains id of the user that added the friend
`friend_user_id` -> contains id of the friend in question
With these two tables I am able to get all the pictures posted by the friend of a user using this sql query :
SELECT *
FROM photos
WHERE uploader IN
(
SELECT friend_user_id
FROM users_friends
WHERE user_id = ':user_id'
)
ORDER BY id DESC
I then have two queries to get the link to the next picture and 2 other queries for the previous one. I have two because one is for when you get at the end of the database to go back to the other end and the other one is for all the other queries. And they look something like this (I'm only posting the next button here)
$sql = "SELECT *
FROM photos
WHERE status = 0 AND id < ".$id." AND Uploader IN (SELECT friend_user_id FROM users_friends WHERE user_id = ".$logedInUser['User_ID'].")
ORDER BY id DESC
LIMIT 1
";
$sql2 ="SELECT *
FROM photos
WHERE id = (SELECT MAX(id) FROM photos WHERE status = '0' AND Uploader IN (SELECT friend_user_id FROM users_friends WHERE user_id = ".$logedInUser['User_ID']."))
ORDER BY id DESC
LIMIT 1
";
$result = $db->sql_query($sql);
if (mysql_num_rows($result)==1) {
$row = $db->sql_fetchrow($result);
return "/photo/".$row["id"]."/".urlFriendly($row["Title"])."/".$filter.$sort;
}
else
{
$result = $db->sql_query($sql2);
$row = $db->sql_fetchrow($result);
return "/photo/".$row["id"]."/".urlFriendly($row["Title"])."/".$filter.sort;
}
I feel like there is a lot of stuff that could be simplified, but I don't yet know how to do it.
I also have the Lower bound of Wilson score sql code to implement for the next/previous button, and it is quite a bit sql code to have to write 4 time. Maybe that I can insert the Wilson score in the photos table, which could simplify a lot the sql code.
It would be helpful if you could post the structure of your table - i.e. what columns/types you have and what each row represents. I'll assume that you have a table where each row represents a photo, and that each photo has an ID, filename, and various other columns that help you filter/sort (such as date, favorite, etc.).
If your filtering and sorting preferences are defined by the user, you can then use those in a single SQL query, whereby you get the full set of ordered results. For instance:
$sort = date;
$filter = favorite;
$sql = "SELECT ID, filename, [other vars here]
FROM photos
WHERE " . $filter . " = TRUE
ORDER BY ". $sort . " DESC";
$result = $db->sql_query($sql);
Now, you can use a function such as mysql_fetch_array to step through each row in your result set and populate some kind of data structure with the current, previous, and next photos.
I have a file hosting site where I provide a point for every unique download to user.
Sample of my table
These points can be redeemed by user. So for example if a user redeems 100 points than what is the best query to reduce points available from each row till 100 points are reduced.
Thank You.
You should create two tables for this:
Table files
- id
- name
- size
Table points
- id
- file_id
(- user)
- points
Insert a new file:
INSERT INTO files (name, size) VALUES ('kat92a.jpg', 105544); // New file with ID 1
Now you can give points to a file, negative or positive:
INSERT INTO points (file_id, points) VALUES (1, 100); //Positive points
INSERT INTO points (file_id, points) VALUES (1, -10); //Negative points
And you can select the total number of points:
SELECT
files.name,
files.size,
(SELECT sum(points) FROM points WHERE file_id = 1) AS points
FROM files
WHERE id = 1
Alright, then, here's the SQL-dumb way I would do it. Hopefully an SQL guru will come around with a better solution. Note: This is pure pseudocode; write your own code based on this--it's not going to work out of the box.
$total_to_deduct = 100;
// Each time, get the row with the highest points
$top_points_query = "SELECT id, points FROM my_table ORDER BY points DESC LIMIT 1;"
do {
$result = do_query($top_points_query);
if($result) {
// I'm assuming you don't want to deduct more points from a row than it has
$num_to_deduct = min($result['points'], $total_to_deduct);
// Now deduct the points from the row we got earlier
$update_query = "UPDATE my_table SET points = points - $num_to_deduct
WHERE id = $result['id']";
if(do_query($update_query)) {
$total_to_deduct -= $num_to_deduct;
}
}
} while($total_to_deduct > 0); // If we still have points to deduct, do it again
Seems like you just need a simple update Statement and allows you to update the row and if it's more than 100 not update it.
update table set points = if( (points+<VALUE>) <= 100,points+<VALUE>,points) where id = <FILE ID>
This will check to see if the points is higher than 100, if it is then the update statement will just return no results. If the value is less than 100, then it will update the table and give you back the amount of rows that were updated.
Just add a column in your user table with the amount of redeemed points. Is that a viable solution for you?
Here is a pure SQL solution, but I warn you that (a) this is untested and (b) it's just a concept.
DECLARE curs CURSOR FOR
SELECT
id,
points,
FROM
points
WHERE
points > 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET remPoints = 0;
OPEN curs;
SET remPoints = 100; /* modify this value, probably in your app */
REPEAT
FETCH curs INTO cId, cPoints;
IF remPoints >= cPoints THEN
UPDATE points SET points = 0 WHERE id = cId;
ELSE
UPDATE points SET points = points - remPoints WHERE id = cId;
END IF;
SET remPoints = remPoints - cPoints;
UNTIL remPoints <= 0;
CLOSE curs;