diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e58fcbf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +html/php_variables/config-db.php diff --git a/README.md b/README.md index 55dd001..012f219 100644 --- a/README.md +++ b/README.md @@ -1 +1,29 @@ -# cyberprank-urban-game \ No newline at end of file +# cyberprank-urban-game + +This repo contains code used for an urban game (Cyberpunk 2077 theme) I organised with my friends. +The general idea was for people to solve riddles which would give them coordinates and those would then lead people to tokens hidden around the city. Registering a token would score a point for your team. + +The code was meant to be quick to develop, so it's rather dirty and full of security weaknesses. It's also my first time ever with PHP so no miracles are to be expected. No automation was also prepared, so installation would require some manual work (you will figure it out): +- create a PostgreSQL database and user +- modify SQL script according to your needs +- run SQL against the database +- modify PHP connection strings to reflect database credentials +- copy website files to a webserver of your choice (ie. nginx + php-fpm) +- have fun + +I was running the game using: +- Cloudflare as a front +- ElephantSQL as database provider +- local storage provider for webserver + +*edit* +My thoughts, todos and players' feedback after we played the game. It's likely that the following will never get done / implemented, but I feel it's still worth noting them down: +- make the interface mobile-friendly, as it's mobiles that people are playing on +- make sure ALL features are working properly on both Android and iOS (screw you, Apple) +- ditch iframes in favor of a more modern and civilised solution (ie. Ajax) +- improve quality of the code in general +- either make coordinates very accurate (six decimals at least) or add location hints to every riddle (ie. "you can find the token by a bench") +- for a game played on foot, a radius of 750m (1000m tops) seems reasonable, 1200-1500m is too big +- 150-180 minutes of gameplay seem reasonable +- teams of 3 seem like a good idea (or teams of 4 if you want to hint that it'd be a good idea to divide and conquer) +- consider moving part of the infra (webserver and db) from a local provider to GCP or AWS (Azure = meh); while costs will remain similar, you'll get all the flexibility you can imagine diff --git a/html/css/main.css b/html/css/main.css new file mode 100644 index 0000000..51d34b7 --- /dev/null +++ b/html/css/main.css @@ -0,0 +1,73 @@ +body { + background-color: #000; +} + +#timer { + text-align: center; + color: #fcee09; + font-size: 16px; + font-weight: normal; + line-height: 24px; +} + +h1 { + color: #fcee09; + text-align: center; + font-size: 30px; +} + +h2 { + color: #fcee09; + text-align: center; + font-size: 18px; +} + +h3 { + color: #fcee09; + font-size: 16px; + font-weight: normal; + line-height: 24px; +} + +.menu { + text-align: center; + font-size: 20px; + text-decoration: none; +} + +.menu ul { + list-style: none; +} + +.menu ul li { + display: inline-block; +} + +.menu ul li a { + color: #fcee09; + text-decoration: none; + padding: 14px; +} + +.menu li a:hover { + background-color: #253b4d; +} + +.menu .active { + color: #de7e35; + font-weight: bold; +} + +td { + padding: 0 15px; + margin-left: auto; + margin-right: auto; +} + +.hidden_by_default { + display: none; +} + +iframe { + width: 50%; +} \ No newline at end of file diff --git a/html/favicon.ico b/html/favicon.ico new file mode 100644 index 0000000..c3ba548 Binary files /dev/null and b/html/favicon.ico differ diff --git a/html/iframe_riddle.php b/html/iframe_riddle.php new file mode 100644 index 0000000..a5761a9 --- /dev/null +++ b/html/iframe_riddle.php @@ -0,0 +1,91 @@ + + + + + Cyberprank 2069 — gra miejska + + + +

+"; + echo "Udaj się pod poniższe współrzędne i odnajdź wlepkę, a następnie wprowadź jej token, aby zdobyć punkt:
"; + echo "$row[1], $row[2] lub ", ''; + echo ''; + echo ''; + + // and load list of teams to choose from + echo '
+
+ + + + + + +
+
+ '; + } + // if solution given is not correct + else + { + echo "Rozwiązanie niepoprawne. Próbuj dalej."; + } + + pg_free_result($result); + pg_free_result($result2); + pg_close($dbconn); +} + +?> +

+ +// handle communication between bottom iframe and top window +// this iframe is in between so it needs to relay messages + + + diff --git a/html/iframe_token.php b/html/iframe_token.php new file mode 100644 index 0000000..3ffb4b3 --- /dev/null +++ b/html/iframe_token.php @@ -0,0 +1,129 @@ + + + + + Cyberprank 2069 — gra miejska + + + +

+= $row[2]) + { + echo " + + "; + } + else + { + // check if token given is correct + if ($row[0] == $riddle_token) + { + $query5 = 'SELECT extract(epoch from finish_hour) FROM timetable LIMIT 1;'; + $result5 = pg_query($query5) or die('Query failed: ' . pg_last_error()); + $row5 = pg_fetch_row($result5); + + // check if game time limit has passed + if (time() > $row5[0]) + { + // this is to communicate with the top window and display proper message via alert + echo " + + "; + } + else + { + $query7 = 'select * from teams where id = $1 and $2=ANY(solved_riddles_by_id)'; + $result7 = pg_query_params($dbconn, $query7, array( $team_id, $riddle_id )) or die('Query failed: ' . pg_last_error()); + $rows7 = pg_num_rows($result7); + + // check if the team has already solved this riddle + if ($rows7 != 0) + { + echo " + + "; + + pg_free_result($result7); + } + else + { + // insert log + $query2 = "INSERT INTO history VALUES (DEFAULT, DEFAULT, $1, $2)"; + $result2 = pg_query_params($dbconn, $query2, array( $team_id, $riddle_id )) or die('Query failed: ' . pg_last_error()); + + // insert point for the given team + $query3 = "UPDATE teams SET points = points + 1 WHERE id = $1"; + $result3 = pg_query_params($dbconn, $query3, array( $team_id )) or die('Query failed: ' . pg_last_error()); + + // update riddle's solve_count + $query4 = "UPDATE riddles SET solve_count = solve_count + 1 WHERE id = $1"; + $result4 = pg_query_params($dbconn, $query4, array( $riddle_id )) or die('Query failed: ' . pg_last_error()); + + // update team's list of solved riddles + $query6 = "UPDATE teams SET solved_riddles_by_id = array_append(solved_riddles_by_id, $1) WHERE id = $2;"; + $result6 = pg_query_params($dbconn, $query6, array( $riddle_id, $team_id )) or die('Query failed: ' . pg_last_error()); + + echo " + + "; + + pg_free_result($result2); + pg_free_result($result3); + pg_free_result($result4); + pg_free_result($result5); + pg_free_result($result6); + } + } + } + else + { + echo " + + "; + } + + pg_free_result($result); + pg_close($dbconn); + } +} + +?> +

+ + diff --git a/html/images/token.JPG b/html/images/token.JPG new file mode 100644 index 0000000..f4143fc Binary files /dev/null and b/html/images/token.JPG differ diff --git a/html/index.php b/html/index.php new file mode 100644 index 0000000..1e8fe4b --- /dev/null +++ b/html/index.php @@ -0,0 +1,93 @@ + + + + + + + Cyberprank 2069 — gra miejska + + + + +

CYBERPRANK 2069

+

welcome to the Krak City!

+

+ + +

+"; + echo "rozwiązana przez: $row[2] (z max $row[3] zespołów)
"; + echo "treść zagadki: \"$row[1]\"
"; + + // check if this riddle has reached it's solve_count limit + if ($row[2] < $row[3]) + { + echo ""; + } + else + { + echo "LIMIT ROZWIĄZAŃ DLA ZAGADKI ZOSTAŁ WYCZERPANY"; + } + + echo '
+
+ + + + + +
+ +
'; + + echo "

"; +} + +$dbconn_time_query = 'SELECT extract(epoch from finish_hour) FROM timetable LIMIT 1;'; +$dbconn_time_query_result = pg_query($dbconn_time_query) or die('Query failed: ' . pg_last_error()); +$game_finish_time = pg_fetch_row($dbconn_time_query_result); + +echo ''; +echo ''; + +pg_free_result($result); +pg_free_result($dbconn_time_query_result); + +pg_close($dbconn); + +?> +

+ + // listen on messages from the bottom iframe and display them via alert + + + + + + diff --git a/html/php_variables/config-db.php.example b/html/php_variables/config-db.php.example new file mode 100644 index 0000000..f59509c --- /dev/null +++ b/html/php_variables/config-db.php.example @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/html/results.php b/html/results.php new file mode 100644 index 0000000..d6c7b8f --- /dev/null +++ b/html/results.php @@ -0,0 +1,68 @@ + + + + + + + Cyberprank 2069 — gra miejska + + + + +

CYBERPRANK 2069

+

welcome to the Krak City!

+

+ + +

+'; + +foreach ($resultArr as $array) +{ + echo ' + ' . $array['name'] . ' + ' . $array['points'] . ' + '; +} +echo ''; + +?> +

+ +var js_game_finish_time = ', $game_finish_time[0], ';'; +echo ' + + + diff --git a/html/rules.php b/html/rules.php new file mode 100644 index 0000000..9289e96 --- /dev/null +++ b/html/rules.php @@ -0,0 +1,61 @@ + + + + + + + Cyberprank 2069 — gra miejska + + + + +

CYBERPRANK 2069

+

welcome to the Krak City!

+

+ + +

+Zasady są następujące:
+- na stronce opublikowano listę tematycznych zagadek
+- zagadki są wspólne dla wszystkich, można rozwiązywać je w dowolnej kolejności
+- w celu rozwiązania zagadki należy użyć guzika i podać rozwiązanie
+- rozwiązania to słowa składające tylko z litery (i ew. cyfr), ale bez polskich znaków diakrytycznych (ą, ę...), spacji i innych znaków specjalnych (?, $...)
+- przykładowe rozwiązanie "Mały Kebab?!" powinno być wpisane jako "malykebab"
+- po rozwiązaniu zagadki otrzymacie koordynaty do odpowiedniej wlepki
+--- wszystkie wlepki rozmieszczono w promieniu do około 1200m od Hexa
+--- wszystkie wlepki rozmieszczono w publicznie dostępnych miejscach na wysokości nie wyższej niż 2m
+- należy dotrzeć do koordynatów wlepki, odnaleźć ją i odczytać znajdujący się na niej token
+- token następnie należy wprowadzić w formularzu pod wybraną zagadką aby zdobyć punkt
+- jeden token może zostać wykorzystany tylko przez trzy grupy, później zagadka wygasa (będzie to odpowiednio oznaczone)
+- po wygaśnięciu czasu gry (około 2.5 godziny, patrz: licznik na górze strony) punkty nie będą już przyznawane
+- po zakończeniu zabawy spotykamy się w Hexie
+ +
Przykładowa wlepka wygląda jak niżej:

+Przykładowy token +

+ +var js_game_finish_time = ', $game_finish_time[0], ';'; +echo ' + + + diff --git a/html/scripts/common.js b/html/scripts/common.js new file mode 100644 index 0000000..a3b1631 --- /dev/null +++ b/html/scripts/common.js @@ -0,0 +1,12 @@ +function openForm(id) { + document.getElementById(id).style.display = "block"; +} + +function closeForm(id) { + document.getElementById(id).style.display = "none"; +} + +var iframe = document.getElementById("iframe"); +iframe.onload = function() { + iframe.style.height = iframe.contentWindow.document.body.scrollHeight + 'px'; +} \ No newline at end of file diff --git a/html/scripts/navigation.js b/html/scripts/navigation.js new file mode 100644 index 0000000..6d83c78 --- /dev/null +++ b/html/scripts/navigation.js @@ -0,0 +1,18 @@ +function navigate(latitude, longitude) { + if(/Android/i.test(navigator.userAgent)) { + // android + var link_to_map = 'KLIKNIJ TUTAJ, ABY OTWORZYĆ NAWIGACJĘ (ANDROID)'; + } else if(/iPhone|iPad|iPod/i.test(navigator.userAgent)) { + // ios +// one of the following should work, however it seems that there are problems and since I have neither an iDevice at home nor time to debug... +// so screw Apple, a regular link would have to suffice +// var link_to_map = 'KLIKNIJ TUTAJ, ABY OTWORZYĆ NAWIGACJĘ (APPLE)'; +// var link_to_map = 'KLIKNIJ TUTAJ, ABY OTWORZYĆ NAWIGACJĘ (APPLE)'; +// var link_to_map = 'KLIKNIJ TUTAJ, ABY OTWORZYĆ NAWIGACJĘ (APPLE)'; + var link_to_map = 'KLIKNIJ TUTAJ, ABY OTWORZYĆ LINK DO MAPY (może nie działać na iPhone)'; + } else { + // other mobile or desktop + var link_to_map = 'KLIKNIJ TUTAJ, ABY OTWORZYĆ GOOGLE MAPS (PRZEGLĄDARKA)'; + } + document.getElementById("navigation").innerHTML = link_to_map; +} diff --git a/html/scripts/timer.js b/html/scripts/timer.js new file mode 100644 index 0000000..bcb1401 --- /dev/null +++ b/html/scripts/timer.js @@ -0,0 +1,26 @@ +// Set the date we're counting down to +var countDownDate = new Date(js_game_finish_time*1000) + +// Update the count down every 1 second +var x = setInterval(function() { + + // Get today's date and time + var now = new Date().getTime(); + + // Find the distance between now and the count down date + var distance = countDownDate - now; + + // Time calculations for days, hours, minutes and seconds + var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + var seconds = Math.floor((distance % (1000 * 60)) / 1000); + + // Display the result in the element with id="timer" + document.getElementById("timer").innerHTML = "pozostało: " + hours + ":" + minutes + ":" + seconds; + + // If the count down is finished, write some text + if (distance < 0) { + clearInterval(x); + document.getElementById("timer").innerHTML = "KONIEC CZASU GRY"; + } +}, 1000); diff --git a/html/sql/init_script.sql b/html/sql/init_script.sql new file mode 100644 index 0000000..0942b28 --- /dev/null +++ b/html/sql/init_script.sql @@ -0,0 +1,117 @@ +-- +-- create riddles +-- + +DROP TABLE IF EXISTS riddles; + +CREATE TABLE riddles ( + id serial PRIMARY KEY, + riddle VARCHAR ( 400 ) NOT NULL, + solution VARCHAR ( 50 ) NOT NULL, + solve_count INTEGER NOT NULL, + max_solve_count INTEGER NOT NULL, + coordinate_x VARCHAR ( 20 ) NOT NULL, + coordinate_y VARCHAR ( 20 ) NOT NULL, + token VARCHAR ( 16 ) UNIQUE NOT NULL +); + +ALTER TABLE riddles OWNER TO cyberprank2069user; + +INSERT INTO riddles VALUES (DEFAULT, 'Jaki zespół nagrał pierwsze kilka oficjalnie opublikowanych utworów z gry?', 'refused', 0, 3, '50.051573', '19.937732', 'tfa18t1l'); +INSERT INTO riddles VALUES (DEFAULT, 'Kto podkładał głos pod trailer Deep Dive? (imię)', 'borys', 0, 3, '50.057749', '19.934992', '235fhhzs'); +INSERT INTO riddles VALUES (DEFAULT, 'Ile lat po swoim papierowym pierwowzorze dzieje się akcja CP2077?', '57', 0, 3, '50.064', '19.934', 'ef26qdmo'); +INSERT INTO riddles VALUES (DEFAULT, 'Jaki napój promuje pani z siusiakiem?', 'chromanticure', 0, 3, '50.065546', '19.939378', 'mw0r68rf'); +INSERT INTO riddles VALUES (DEFAULT, 'Jak nazywa się seria filmików promujących CP2077?', 'nightcitywire', 0, 3, '50.066499', '19.941861', 'cqkis9t6'); + +INSERT INTO riddles VALUES (DEFAULT, 'Jak na drugie imię ma Mike P.?', 'alyn', 0, 3, '50.060838', '19.9393028', '40bpmfrn'); +INSERT INTO riddles VALUES (DEFAULT, 'W jaki dzień tygodnia i miesiąca opublikowano pierwszy teaser CP2077 od Platige? (np. poniedzialek15)', 'czwartek10', 0, 3, '50.060374', '19.941543', 'o4ady4lb'); +INSERT INTO riddles VALUES (DEFAULT, 'W jakim mieście toczy się akcja CP2077?', 'nightcity', 0, 3, '50.055507', '19.942922', 'qkgzmvq4'); +INSERT INTO riddles VALUES (DEFAULT, 'Jak ma na nazwisko modelka pozująca jako dziewczyna z ostrzami do teasera do CP2077 od Platige?', 'danysz', 0, 3, '50.053', '19.945', '59lhjwhv'); +INSERT INTO riddles VALUES (DEFAULT, 'Jakie zwierzę użycza imienia tym ostrzom? (po polsku)', 'modliszka', 0, 3, '50.047', '19.947', 'l182tm65'); + +INSERT INTO riddles VALUES (DEFAULT, 'Jaki napis na kurtce ma dziewczyna na oficjalnym plakacie promującym styl Kitsch?', 'getrichordietrying', 0, 3, '50.054164', '19.949893', '2qansgzx'); +INSERT INTO riddles VALUES (DEFAULT, 'Jaka była pierwotna data premiery CP2077? (np. 20151201)', '20200416', 0, 3, '50.055291', '19.954447', 'o1wup5l8'); +INSERT INTO riddles VALUES (DEFAULT, 'Kto użyczy głosu polskiemu Johnny''emu w CP2077?', 'michalzebrowski', 0, 3, '50.057169', '19.959793', 'vj4jhlmf'); +INSERT INTO riddles VALUES (DEFAULT, 'Jaką ksywkę ma postać z CP2077 nosząca na butach wiedźmińskie emblematy?', 'dex', 0, 3, '50.062368', '19.961115', 'up8zw3mz'); +INSERT INTO riddles VALUES (DEFAULT, 'Ile minut trwał walkthrough CP2077 z 2018?', '48', 0, 3, '50.068', '19.953', 'yqllrwdf'); + +INSERT INTO riddles VALUES (DEFAULT, 'Jakiej marki samochodem porusza się V w trailerach?', 'quadra', 0, 3, '50.066622', '19.949637', 'x0tbjlnf'); +INSERT INTO riddles VALUES (DEFAULT, 'W którym roku umieszczona jest akcja pierwszej papierowej gry z serii Cyberpunk?', '2013', 0, 3, '50.064568', '19.957295', 'wu0v8a4y'); +INSERT INTO riddles VALUES (DEFAULT, 'Jak ma na imię znany z trailerów, wtrącający hiszpańskojęzyczne wstawki, przyjaciel V?', 'jackie', 0, 3, '50.063897', '19.946614', '3rwdter7'); +INSERT INTO riddles VALUES (DEFAULT, 'Osiemset jednostek czego podał lekarz Trauma Team pacjentce NC570442? (po angielsku)', 'fibrin', 0, 3, '50.061131', '19.944445', 'v10kk5sv'); +INSERT INTO riddles VALUES (DEFAULT, 'Kto będzie polskim wydawcą CP2077?', 'cenega', 0, 3, '50.060656', '19.949094', 'j4ysn01u'); + +INSERT INTO riddles VALUES (DEFAULT, 'W którym roku Keanu powiedział fanom, że są breathtaking?', '2019', 0, 3, '50.058545', '19.954118', '6p8ltagy'); +INSERT INTO riddles VALUES (DEFAULT, 'Co chce spalić pan Silverhand?', 'miasto', 0, 3, '50.057537', '19.949193', 'y49bal76'); +INSERT INTO riddles VALUES (DEFAULT, 'Na jakim paliwie jeżdżą samochody w świecie Cyberpunka?', 'chooh2', 0, 3, '50.056279', '19.953753', 'eswnet8l'); +INSERT INTO riddles VALUES (DEFAULT, 'Jaki napis na kołnierzu swojej kurtki ma V?', 'samurai', 0, 3, '50.059425', '19.949119', 'zkeu8rrl'); + +-- +-- create teams +-- + +DROP TABLE IF EXISTS teams; + +CREATE TABLE teams ( + id serial PRIMARY KEY, + name VARCHAR ( 200 ) UNIQUE NOT NULL, + points INTEGER NOT NULL, + solved_riddles_by_id INTEGER[] +); + +ALTER TABLE teams OWNER TO cyberprank2069user; + +INSERT INTO teams VALUES (DEFAULT, 'Tyger Claws', '0'); + +INSERT INTO teams VALUES (DEFAULT, 'Animals', '0'); + +INSERT INTO teams VALUES (DEFAULT, 'Valentinos', '0'); + +INSERT INTO teams VALUES (DEFAULT, 'Mox', '0'); + +INSERT INTO teams VALUES (DEFAULT, 'Voodoo Boys', '0'); + +INSERT INTO teams VALUES (DEFAULT, '6th Street', '0'); + +INSERT INTO teams VALUES (DEFAULT, 'Maelstrom', '0'); + +-- +-- create history +-- + +DROP TABLE IF EXISTS history; + +CREATE TABLE history ( + id serial PRIMARY KEY, + timestamp timestamptz DEFAULT NOW(), + team_id INTEGER NOT NULL, + riddle_id INTEGER NOT NULL +); + +ALTER TABLE history OWNER TO cyberprank2069user; + +-- +-- create timetable +-- + +DROP TABLE IF EXISTS timetable; + +CREATE TABLE timetable ( + id serial PRIMARY KEY, + finish_hour timestamptz +); + +ALTER TABLE timetable OWNER TO cyberprank2069user; + + +-- +-- use this to configure timer +-- TRUNCATE TABLE timetable; +-- INSERT INTO timetable VALUES (DEFAULT, NOW() + INTERVAL '2 hours'); +-- + + +-- +-- use this to check for history if you wish +-- SELECT history.id, history.timestamp, history.riddle_id, teams.name FROM history INNER JOIN teams ON history.team_id = teams.id; +--