Private
Public Access
1
0

Compare commits

...

8 Commits

Author SHA1 Message Date
247f437445 fix: pkg: the font-awesome simplifying to work on bare-metal - & fix all warnings at build time #4
All checks were successful
Deploy to Production / deploy (push) Successful in 14s
2026-04-18 11:42:46 +02:00
0e94367223 new: usr: add rules page #4 2026-04-18 11:11:52 +02:00
a9ee28b395 fix: usr: the css problem had been solved on reponsive gfx on homepage #4 2026-04-18 10:34:46 +02:00
bd074c5c9d chg: pkg: new version release !skipChangelog 2026-04-18 08:49:59 +02:00
42c552c528 fix: usr: quickfix for https-only login - & add user data when the user is not logged in #4
All checks were successful
Deploy to Production / deploy (push) Successful in 1m55s
2026-04-18 08:49:10 +02:00
3b376e5386 chg: pkg: new version release !skipChangelog 2026-04-16 11:56:30 +02:00
45a8e6b4a1 chg: dev: add consent checkbox to user's registration - and fix the sharing pics #4
All checks were successful
Deploy to Production / deploy (push) Successful in 15s
2026-04-16 11:56:10 +02:00
1f8e9c3c56 chg: pkg: add correct version numbering and CHANGELOG - and add the LICENSE #4 2026-04-16 11:35:53 +02:00
40 changed files with 880 additions and 262 deletions

32
.gitchangelog.rc Normal file
View File

@@ -0,0 +1,32 @@
ignore_regexps = [
r'@minor', r'!minor',
r'@cosmetic', r'!cosmetic',
r'@refactor', r'!refactor',
r'@wip', r'!wip',
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
r'^$', ## ignore commits with empty messages
r'@skipChangelog', r'!skipChangelog', r'skipChangeLog', r'!skipChangeLog',
r'Merge branch', r'Merge remote-tracking branch', r'!deploy',
]
section_regexps = [
('New', [
r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('Changes', [
r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('Fix', [
r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('Other', None ## Match all lines
),
]
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
subject_process = (strip |
ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
SetIfEmpty("No commit message.") | ucfirst | final_dot)
tag_filter_regexp = r'^(v)?[0-9]+\.[0-9]+(\.[0-9]+)?(\-[0-9]+)?$'
unreleased_version_label = "(unreleased)"
output_engine = mustache("markdown")
include_merge = True
revs = []

View File

@@ -1,200 +1,390 @@
Changelog # Changelog
=========
(unreleased) ## v2026.2.1-8 (2026-04-18)
------------
New ### Fix
~~~
- Add notification email when a user is registered #4. [Lang]
- Add Contact page with email sending behaviour #4. [Lang]
- Add timer for the acceptance of the challenge #4. [Lang]
- Registered users have avatars next to the timer #4. [Lang]
- Add opportunity to use profile picture. #4. [Lang]
- Add more stats and a dialog for the recent battle that can be
shareable #4. [Lang]
- Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
- Add beta logo to the corner #3. [Lang]
- Add mineseeker game to the symfony 4 project #3. [Lang]
- Upgrade to the latest symfony v4 #3. [Lang]
Changes * Quickfix for https-only login - & add user data when the user is not logged in #4. [Lang]
~~~~~~~
- Add notification on activation too #4. [Lang]
- Change the shareable battle - add avatars to it - even on the og tags
#4. [Lang]
- Change text #4. [Lang]
- Add donation button #4. [Lang]
- Protect the gameplay with recaptcha #4. [Lang]
- The waiting dialog is uncloseable until the time is up #4. [Lang]
- Add share button to the overlay when the game ends #4. [Lang]
- Make fancy og tags - and create a special one for battle sharing #4.
[Lang]
- The user's avatar will be saved as a uuid.extension #4. [Lang]
- Fix missing favicon #4. [Lang]
- Add modern Webauthn authentication #4. [Lang]
- Refactor all forms to have Symfony Form Types & Validation
Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
- Add forgot password functionality #4. [Lang]
- Increase the minimum PHP version to the latest major - and massive
refactor on back-end, like Controllers and Repositories #4. [Lang]
- Redesign the resign dialog #4. [Lang]
- Re-implement the waiting for opponent dialog - refactor its gfx - &
add online user selection dialog #4. [Lang]
- Improve the gfx on homepage - implement login/register and activation
for authentication - and add the first version of profile page #4.
[Lang]
- Refactor and redesign the gfx on front-end #4. [Lang]
- Add timers to each player - renew the whole migration #4. [Lang]
- Use namespaces for front-end #4. [Lang]
- Replace webpack w/ vite & remove old, legacy jQuery from the code #4.
[Lang]
- More, massive refactor for front-end #4. [Lang]
- Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
- Change the code style to fit the current standard #4. [Lang]
- Refactor to use Attributes instead of yaml markdown #4. [Lang]
- Outsource the Grid generation and interactions to the backend #4.
[Lang]
- Remove unnecessary variables and prune the Facebook registration
method #4. [Lang]
- Replace the legacy gos/web-socket-bundle & replace it with Mercure
protocol #4. [Lang]
- Created the first working solution since 7 yrs #4. [Lang]
- Make the first working version - the stepping is broken due to the
algorythm structure #4. [Lang]
- Change the composer default php minimum environment #3. [Lang]
- Change the default url to wss on frontend #3. [Lang]
- Refactor Rpc and Topic classes #3. [Lang]
- Refactor classes and reformat some layout #3. [Lang]
- Remove deprecated files #3. [Lang]
- Doc in README.md #3. [Lang]
- Gitignore a js.map file #2. [Lang]
Fix
~~~
- The meta tags does not have https scheme - nothing worked in
configuration #4. [Lang]
- Another attempt to fix the email assets #4. [Lang]
- The images does not shows in emails #4. [Lang]
- Missing font-awesome icons on bare-metal environment #4. [Lang]
- Quickfix for email sending #4. [Lang]
Other
~~~~~
- Hg: pkg: new version release !skipChangelog. [Lang]
- Pkg: usr: solve the not-working mailing on dev env under docker #4.
[Lang]
- Deploy version 1.1.0 !deploy #11. [Lang]
1.1.0 (2019-10-26) ## v2026.2.1-7 (2026-04-16)
------------------
Changes ### Changes
~~~~~~~
- Reinit project - disable redis module and make the project compatible * Add consent checkbox to user's registration - and fix the sharing pics #4. [Lang]
w/ PHP7.3 #2. [Lang]
* Add correct version numbering and CHANGELOG - and add the LICENSE #4. [Lang]
0.4.0 (2019-10-26) ## v2026.2.1-6 (2026-04-16)
------------------
- Change session driver to REDIS. [Lang] ### Changes
- Add created, updated field to db && improve graph design. [Lang]
- Cache setup && optimalize for google pagespeed && optimalize all * Update all texts on all pages - extend them with the game specific things #4. [Lang]
images. [Lang]
- Improve graph design on homepage && add footer and techs && add
official pages. [Lang] ## v2026.2.1-5 (2026-04-16)
- Bugfix mine websocket periodic mysql calling. [Lang]
- Bugfix hwioauth remember me && centralize hwioauth and facebook ### Fix
settings. [Lang]
- Centralize jquery && bugfix mysql auto-termination problem w/ user * The meta tags does not have https scheme - nothing worked in configuration #4. [Lang]
auth. [Lang]
- Release beta4. [Lang]
- Gitignore npm debug log. [Lang] ## v2026.2.1-4 (2026-04-15)
- Add english lang everywhere && add snowfall && add centralized version
nbr && improve stylesheet && slack integration. [Lang] ### New
- Bugfix #30 && random bg in game. [Lang]
- Add google analytics and facebook scripts && improve url share method * Add notification email when a user is registered #4. [Lang]
w/ fb && enforce https in prod. [Lang]
- Reg and login buttons on index && remove list method && facebook ### Changes
centralize. [Lang]
- Redesign user frontend. [Lang] * Add notification on activation too #4. [Lang]
- Mods for performance; one js.min file on prod. [Lang]
- Improve webpack config for prod compile #23. [Lang]
- Ssl handling #22 && reconnection issues #20, #21. [Lang] ## v2026.2.1-2 (2026-04-15)
- Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
- Refact && game reconnection and restore w/o refresh #3 && bugfix bomb ### Fix
explosion on opponent mines #19. [Lang]
- Typo in rpc. [Lang] * Another attempt to fix the email assets #4. [Lang]
- Handle prod mysql timeout && graphics improve. [Lang]
- Gitignore webpacked index.js. [Lang]
- Add production mods. [Lang] ## v2026.2.1-1 (2026-04-15)
- Bugfix points saving and exploded bombs to db && you can resign #6.
[Lang] ### Fix
- Bugfix resign button existence #11. [Lang]
- Bugfix opponent bomb btn buzz on hover #10. [Lang] * The images does not shows in emails #4. [Lang]
- Bugfix points problem in the end #16. [Lang]
- Add desc to every user #9. [Lang]
- Clipboard - not working #8. [Lang] ## v2026.2.1-0 (2026-04-15)
- Random player on start #5. [Lang]
- Show left mines after end #2 && reduce network traffic && better ### New
active field checking method. [Lang]
- Some refactor #13. [Lang] * Add Contact page with email sending behaviour #4. [Lang]
- Bugfix grid field render #12. [Lang]
- Game ends after x mines. [Lang] ### Changes
- Add new sounds && refactor && new bg images && form redesigns. [Lang]
- Bugfix entities gridrow, grid && improve graph design on homepage. * Add missing .env variable and increase the version number and add missing data from front-end and back-end deps descriptor #4. [Lang]
[Lang]
- Some refactor && prod settings. [Lang] * Change the shareable battle - add avatars to it - even on the og tags #4. [Lang]
- Improve graphics design in game. [Lang]
- Bugfix grid row in entity. [Lang] * Change text #4. [Lang]
- Bugfix changePlayer after bomb explosion. [Lang]
- Improve game graph design. [Lang] ### Fix
- Login and register form more design. [Lang]
- Add basic design to userbundle && refactor. [Lang] * The mailhog is crashed on development env #4. [Lang]
- Add font-awesome. [Lang]
- Working user authentication w/ fb and plain login. [Lang] * The og tags did not have proper http schema - they should have https #4. [Lang]
- Add facebook login module, hwi/HWIOAuthBundle. [Lang]
- Login && register form overrided. [Lang]
- Js and config refactor. [Lang] ## v2026.2.0-5 (2026-04-14)
- Replace gridcol object to json array in db. [Lang]
- Refactor. [Lang] ### Changes
- Save steps and point info to db. [Lang]
- Save the step data to db. [Lang] * Add donation button #4. [Lang]
- Renamed the acme to mineseeker && handle when the user connection has
been lost. [Lang]
- Add player names to UI. [Lang] ## v2026.2.0-4 (2026-04-14)
- Add overlay && game do not start until the opponent came. [Lang]
- Add base64 encryption to grid when it has been sended to server. ### New
[Lang]
- On click opponents bomb, you cannot target && refactor. [Lang] * Add timer for the acceptance of the challenge #4. [Lang]
- Warning when player has been found more than 20 mines. [Lang]
- Bugfix center mine counter animation. [Lang] ### Changes
- The opponent is the next when bomb is exploded. [Lang]
- Current username checked && refactor && remove players in channel when * Protect the gameplay with recaptcha #4. [Lang]
they are more than 2. [Lang]
- Send bomb info and use it on opponent. [Lang] * The waiting dialog is uncloseable until the time is up #4. [Lang]
- Add sounds w/ howler. [Lang]
- Bugfix multiple empty fields w/ one click on opponent view. [Lang] * Add share button to the overlay when the game ends #4. [Lang]
- Refact && remove sound and logging && bugfix BIGBUG - handleGridField
and showAppropriateFields sort order... [Lang] * Make fancy og tags - and create a special one for battle sharing #4. [Lang]
- Create first working communication. [Lang]
- Create entities and repositories. [Lang] ### Fix
- Changed websocket default port && debug RPC. [Lang]
- Created working session and client handler w/ websocket. [Lang] * Missing font-awesome icons on bare-metal environment #4. [Lang]
- Working websocket client and server w/o session handling and storage.
[Lang]
- Composer update. [Lang] ## v2026.2.0-3 (2026-04-14)
- Improve game && start sound creating. [Lang]
- Refactor grid control and grid field. [Lang] ### Changes
- Created basic game w/ table and animations. [Lang]
- Websocket basic setup FE & BE && working basic game w/ react && * The user's avatar will be saved as a uuid.extension #4. [Lang]
webpack & babel config. [Lang]
- Gitignore node_modules && add symlink to node_modules (just for
install) && basic react. [Lang] ## v2026.2.0-1 (2026-04-14)
- Add react hello world. [Lang]
- Rename project in config. [Lang] ### Fix
- Initial commit && create project in symfony3. [Lang]
* Quickfix for email sending #4. [Lang]
## v2026.2.0-0 (2026-04-14)
### New
* Registered users have avatars next to the timer #4. [Lang]
* Add opportunity to use profile picture. #4. [Lang]
* Add more stats and a dialog for the recent battle that can be shareable #4. [Lang]
* Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
* Add beta logo to the corner #3. [Lang]
* Add mineseeker game to the symfony 4 project #3. [Lang]
* Upgrade to the latest symfony v4 #3. [Lang]
### Changes
* Implement CD script to Gitea and add docs to the process #4. [Lang]
* Remove unnecessary cdn based fonts #4. [Lang]
* Update docs #4. [Lang]
* Add JWT generation script to make Mercure safe #4. [Lang]
* Fix missing favicon #4. [Lang]
* Make compatible the whole project with bare metal AND with docker #4. [Lang]
* Add modern Webauthn authentication #4. [Lang]
* Refactor all forms to have Symfony Form Types & Validation Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
* Add forgot password functionality #4. [Lang]
* Increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4. [Lang]
* Redesign the resign dialog #4. [Lang]
* Re-implement the waiting for opponent dialog - refactor its gfx - & add online user selection dialog #4. [Lang]
* Improve the gfx on homepage - implement login/register and activation for authentication - and add the first version of profile page #4. [Lang]
* Refactor and redesign the gfx on front-end #4. [Lang]
* Upgrade to the latest LTS Symfony package and backend #4. [Lang]
* Add timers to each player - renew the whole migration #4. [Lang]
* Update the vite related stuff because CORS and React errors - reinit the miration #4. [Lang]
* Use namespaces for front-end #4. [Lang]
* Replace webpack w/ vite & remove old, legacy jQuery from the code #4. [Lang]
* More, massive refactor for front-end #4. [Lang]
* Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
* Change the code style to fit the current standard #4. [Lang]
* Refactor to use Attributes instead of yaml markdown #4. [Lang]
* Outsource the Grid generation and interactions to the backend #4. [Lang]
* Remove unnecessary variables and prune the Facebook registration method #4. [Lang]
* Replace the legacy gos/web-socket-bundle & replace it with Mercure protocol #4. [Lang]
* Make a massive refactor to the backend and remove all unnecessary deps - and make small refactors for the frontend too #4. [Lang]
* Created the first working solution since 7 yrs #4. [Lang]
* Add some changes on BE - add eslint and editorconfig - and add some deps #4. [Lang]
* Make the first working version - the stepping is broken due to the algorythm structure #4. [Lang]
* Change the composer default php minimum environment #3. [Lang]
* Change the default url to wss on frontend #3. [Lang]
* Refactor Rpc and Topic classes #3. [Lang]
* Refactor classes and reformat some layout #3. [Lang]
* Remove deprecated files #3. [Lang]
* Doc in README.md #3. [Lang]
* Gitignore a js.map file #2. [Lang]
### Other
* Pkg: usr: solve the not-working mailing on dev env under docker #4. [Lang]
## 1.1.0 (2019-10-26)
### Changes
* Reinit project - disable redis module and make the project compatible w/ PHP7.3 #2. [Lang]
## 0.4.0 (2019-10-26)
### Other
* Change session driver to REDIS. [Lang]
* Add created, updated field to db && improve graph design. [Lang]
* Cache setup && optimalize for google pagespeed && optimalize all images. [Lang]
* Improve graph design on homepage && add footer and techs && add official pages. [Lang]
* Bugfix mine websocket periodic mysql calling. [Lang]
* Bugfix hwioauth remember me && centralize hwioauth and facebook settings. [Lang]
* Centralize jquery && bugfix mysql auto-termination problem w/ user auth. [Lang]
* Release beta4. [Lang]
* Gitignore npm debug log. [Lang]
* Add english lang everywhere && add snowfall && add centralized version nbr && improve stylesheet && slack integration. [Lang]
* Bugfix #30 && random bg in game. [Lang]
* Add google analytics and facebook scripts && improve url share method w/ fb && enforce https in prod. [Lang]
* Reg and login buttons on index && remove list method && facebook centralize. [Lang]
* Redesign user frontend. [Lang]
* Mods for performance; one js.min file on prod. [Lang]
* Improve webpack config for prod compile #23. [Lang]
* Ssl handling #22 && reconnection issues #20, #21. [Lang]
* Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
* Refact && game reconnection and restore w/o refresh #3 && bugfix bomb explosion on opponent mines #19. [Lang]
* Typo in rpc. [Lang]
* Handle prod mysql timeout && graphics improve. [Lang]
* Gitignore webpacked index.js. [Lang]
* Add production mods. [Lang]
* Bugfix points saving and exploded bombs to db && you can resign #6. [Lang]
* Bugfix resign button existence #11. [Lang]
* Bugfix opponent bomb btn buzz on hover #10. [Lang]
* Bugfix points problem in the end #16. [Lang]
* Add desc to every user #9. [Lang]
* Clipboard - not working #8. [Lang]
* Random player on start #5. [Lang]
* Show left mines after end #2 && reduce network traffic && better active field checking method. [Lang]
* Some refactor #13. [Lang]
* Bugfix grid field render #12. [Lang]
* Game ends after x mines. [Lang]
* Add new sounds && refactor && new bg images && form redesigns. [Lang]
* Bugfix entities gridrow, grid && improve graph design on homepage. [Lang]
* Some refactor && prod settings. [Lang]
* Improve graphics design in game. [Lang]
* Bugfix grid row in entity. [Lang]
* Bugfix changePlayer after bomb explosion. [Lang]
* Improve game graph design. [Lang]
* Login and register form more design. [Lang]
* Add basic design to userbundle && refactor. [Lang]
* Add font-awesome. [Lang]
* Working user authentication w/ fb and plain login. [Lang]
* Add facebook login module, hwi/HWIOAuthBundle. [Lang]
* Login && register form overrided. [Lang]
* Js and config refactor. [Lang]
* Replace gridcol object to json array in db. [Lang]
* Refactor. [Lang]
* Save steps and point info to db. [Lang]
* Save the step data to db. [Lang]
* Renamed the acme to mineseeker && handle when the user connection has been lost. [Lang]
* Add player names to UI. [Lang]
* Add overlay && game do not start until the opponent came. [Lang]
* Add base64 encryption to grid when it has been sended to server. [Lang]
* On click opponents bomb, you cannot target && refactor. [Lang]
* Warning when player has been found more than 20 mines. [Lang]
* Bugfix center mine counter animation. [Lang]
* The opponent is the next when bomb is exploded. [Lang]
* Current username checked && refactor && remove players in channel when they are more than 2. [Lang]
* Send bomb info and use it on opponent. [Lang]
* Add sounds w/ howler. [Lang]
* Bugfix multiple empty fields w/ one click on opponent view. [Lang]
* Refact && remove sound and logging && bugfix BIGBUG - handleGridField and showAppropriateFields sort order... [Lang]
* Create first working communication. [Lang]
* Create entities and repositories. [Lang]
* Changed websocket default port && debug RPC. [Lang]
* Created working session and client handler w/ websocket. [Lang]
* Working websocket client and server w/o session handling and storage. [Lang]
* Composer update. [Lang]
* Improve game && start sound creating. [Lang]
* Refactor grid control and grid field. [Lang]
* Created basic game w/ table and animations. [Lang]
* Websocket basic setup FE & BE && working basic game w/ react && webpack & babel config. [Lang]
* Gitignore node_modules && add symlink to node_modules (just for install) && basic react. [Lang]
* Add react hello world. [Lang]
* Rename project in config. [Lang]
* Initial commit && create project in symfony3. [Lang]

124
LICENSE Normal file
View File

@@ -0,0 +1,124 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2026 SplendidBear (https://www.splendidbear.org)
MineSeeker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MineSeeker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
---
TERMS AND CONDITIONS:
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works.
"The Program" refers to any copyrightable work licensed under this License.
"You" refers to each licensee.
"Legal entities" means the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
"Modify" means to copy from or adapt all or part of the work in a fashion
requiring copyright permission, other than the making of an exact copy.
1. Source Code.
The "source code" for a work means the preferred form of the work for making
modifications to it. "Object code" means any non-source form of a work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright
on the Program. You are granted all permissions necessary to run, modify and
propagate covered works by this License.
3. Copyleft - Derivative Works.
If you modify the Program, your modified version must:
- Carry prominent notices stating that you have modified it
- License the entire work under this License or a compatible license
- Make the source code available to recipients
- Preserve all notices of previous licensing
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it,
provided that you:
- Keep intact all notices of authorship and licensing
- Give recipients access to the source code along with this License
- Do not modify anything except the License itself
5. Conveying Modified Source Versions.
You may convey a work based on the Program under this License provided that:
- The work must be licensed as a whole under this License
- You must give prominent notice of any modifications
- You must provide access to the Corresponding Source code
- You preserve all licensing notices
6. Conveying Non-Source Forms.
If you convey object code or compiled versions, you must also provide:
- The Corresponding Source code (in machine-readable form)
- A notice of the terms under which it is licensed
7. Additional Terms.
No additional restrictions may be placed on the exercise of the rights
granted or affirmed under this License.
8. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
9. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR
THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
---
For the complete GPL-3.0-or-later license text, visit:
https://www.gnu.org/licenses/gpl-3.0.html
For more information about GNU GPL, visit:
https://www.gnu.org/licenses/
MineSeeker is a multiplayer minesweeper game inspired by MSN Messenger's game.
Project: https://www.mineseeker.hu
Author: SplendidBear (https://www.splendidbear.org)

View File

@@ -11,6 +11,7 @@ help:
@echo " make down - Stop and remove containers/networks" @echo " make down - Stop and remove containers/networks"
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)" @echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)" @echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
@echo " make ccp - Clear the production cache"
start: start:
docker compose up -d docker compose up -d
@@ -51,3 +52,6 @@ db-reset:
bin/console doctrine:database:drop --force --if-exists --no-interaction bin/console doctrine:database:drop --force --if-exists --no-interaction
bin/console doctrine:database:create --if-not-exists --no-interaction bin/console doctrine:database:create --if-not-exists --no-interaction
bin/console doctrine:migrations:migrate --no-interaction bin/console doctrine:migrations:migrate --no-interaction
ccp:
bin/console cache:clear --no-warmup --env=prod

View File

@@ -7,9 +7,4 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
$font-path: "/build/webfonts"; @import '@fortawesome/fontawesome-free/css/all.min.css';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/brands';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/regular';

View File

@@ -1,14 +1,15 @@
.hero-auth { #hero-auth {
position: absolute; padding: 20px;
top: 28px;
right: 36px; .hero-auth {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end;
gap: 10px; gap: 10px;
z-index: 10; z-index: 10;
} }
.hero-auth-user { .hero-auth-user {
font: 600 13px 'Rajdhani', sans-serif; font: 600 13px 'Rajdhani', sans-serif;
color: rgba(149, 207, 245, 0.75); color: rgba(149, 207, 245, 0.75);
letter-spacing: 0.5px; letter-spacing: 0.5px;
@@ -17,6 +18,13 @@
gap: 6px; gap: 6px;
i { font-size: 15px; } i { font-size: 15px; }
}
@media screen and (max-width: 1100px) {
.hero-auth {
justify-content: center;
}
}
} }
.hero-auth-btn { .hero-auth-btn {

View File

@@ -33,3 +33,11 @@ main div.txt a {
&:hover { color: #c5e8ff; } &:hover { color: #c5e8ff; }
} }
main div.txt img {
border-radius: 10px;
}
main div.txt .img-container {
text-align: center;
}

View File

@@ -14,7 +14,6 @@ footer {
gap: 40px; gap: 40px;
} }
// Left: brand block
.footer-brand { .footer-brand {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -55,7 +54,6 @@ footer {
line-height: 1.5; line-height: 1.5;
} }
// Right: navigation
.footer-nav-label { .footer-nav-label {
font: 700 11px 'Rajdhani', sans-serif; font: 700 11px 'Rajdhani', sans-serif;
text-transform: uppercase; text-transform: uppercase;
@@ -91,7 +89,6 @@ footer {
} }
} }
// Bottom copyright bar
.footer-copy { .footer-copy {
border-top: 1px solid rgba(255, 255, 255, 0.05); border-top: 1px solid rgba(255, 255, 255, 0.05);
padding: 16px 60px; padding: 16px 60px;

View File

@@ -17,7 +17,6 @@ main {
} }
.mine-container { .mine-container {
background: url("/images/bg-mineseeker-0-outbg.jpg") no-repeat;
background-size: cover; background-size: cover;
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -78,8 +78,6 @@
} }
#mine-wrapper .grid .field-wrapper .field .field-corner { #mine-wrapper .grid .field-wrapper .field .field-corner {
background: url('/images/bg-corner-outbg.png') no-repeat top left;
background-size: 100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View File

@@ -320,7 +320,6 @@ footer nav ul li {
} }
footer nav ul li:nth-child(even) { footer nav ul li:nth-child(even) {
width: 50px;
text-align: center; text-align: center;
} }
@@ -401,8 +400,4 @@ footer nav ul li a:hover {
footer nav ul li { footer nav ul li {
display: block; display: block;
} }
footer nav ul li:nth-child(even) {
display: none;
}
} }

View File

@@ -53,7 +53,10 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
/> />
)} )}
<div className={fieldClass}> <div className={fieldClass}>
<div className="field-corner"> <div
style={{ background: "url('/images/bg-corner-outbg.png') no-repeat top left / 100% 100%" }}
className="field-corner"
>
{isNaN(currentImage) && ( {isNaN(currentImage) && (
<div className="flag-mine"> <div className="flag-mine">
<img src={currentImage} alt="" /> <img src={currentImage} alt="" />

View File

@@ -33,7 +33,11 @@ services:
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_ENDPOINT: http://minio:9000 MINIO_ENDPOINT: http://minio:9000
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9000} MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-http://localhost:9000}
TRUSTED_PROXIES: ${TRUSTED_PROXIES} # IMPORTANT: Set TRUSTED_PROXIES to your reverse proxy IP in production.
# For Docker on same host, use: 172.17.0.1 (default bridge) or 172.16.0.0/12 (overlay network)
# For Kubernetes or external proxy, use the proxy's IP address.
# WARNING: Using 0.0.0.0/0 is insecure in production environments!
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1}
volumes: volumes:
- app_var:/app/var - app_var:/app/var
- caddy_data:/data - caddy_data:/data

View File

@@ -23,12 +23,14 @@ security:
auth_code_parameter_name: _auth_code auth_code_parameter_name: _auth_code
post_only: true post_only: true
default_target_path: MineSeekerBundle_homepage default_target_path: MineSeekerBundle_homepage
always_use_default_target_path: false
prepare_on_login: true prepare_on_login: true
prepare_on_access_denied: true prepare_on_access_denied: true
form_login: form_login:
login_path: MineSeekerBundle_login login_path: MineSeekerBundle_login
check_path: MineSeekerBundle_login check_path: MineSeekerBundle_login
default_target_path: MineSeekerBundle_homepage default_target_path: MineSeekerBundle_homepage
always_use_default_target_path: false
username_parameter: _username username_parameter: _username
password_parameter: _password password_parameter: _password
enable_csrf: true enable_csrf: true

View File

@@ -45,7 +45,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"watch": "vite build --watch", "watch": "vite build --watch",
"build": "vite build && cp -r node_modules/@fortawesome/fontawesome-free/webfonts public/build/webfonts", "build": "vite build",
"lint": "eslint assets/js/" "lint": "eslint assets/js/"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -111,6 +111,12 @@ class GameController extends AbstractController
return $this->render('Official/landing.html.twig'); return $this->render('Official/landing.html.twig');
} }
#[Route('/rules', name: 'MineSeekerBundle_rules')]
public function rules(): Response
{
return $this->render('Official/rules.html.twig');
}
public function sendMail(MailerInterface $mailer, ContactMessage $contactMessage): void public function sendMail(MailerInterface $mailer, ContactMessage $contactMessage): void
{ {
try { try {

View File

@@ -64,7 +64,7 @@ class MercureController extends AbstractController
#[Route('/api/game/join/{gameAssoc}', name: 'MineSeekerBundle_api_game_join', methods: ['POST'])] #[Route('/api/game/join/{gameAssoc}', name: 'MineSeekerBundle_api_game_join', methods: ['POST'])]
public function join(string $gameAssoc, Request $request): JsonResponse public function join(string $gameAssoc, Request $request): JsonResponse
{ {
$this->topicManager->subscribe($gameAssoc, $this->resolveUserName($request), $this->getUser()); $this->topicManager->subscribe($gameAssoc, $this->resolveUserName($request), $this->getUser(), $request);
return $this->json(['success' => true]); return $this->json(['success' => true]);
} }

View File

@@ -17,6 +17,7 @@ use App\Form\ResetPasswordFormType;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use DateTime; use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\Autowire;
@@ -64,7 +65,7 @@ class SecurityController extends AbstractController
#[Route('/logout', name: 'MineSeekerBundle_logout', methods: ['POST'])] #[Route('/logout', name: 'MineSeekerBundle_logout', methods: ['POST'])]
public function logout(): never public function logout(): never
{ {
throw new \LogicException('This action is intercepted by the security firewall.'); throw new LogicException('This action is intercepted by the security firewall.');
} }
#[Route('/register', name: 'MineSeekerBundle_register')] #[Route('/register', name: 'MineSeekerBundle_register')]

View File

@@ -78,6 +78,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TotpTwo
#[Column(length: 255, nullable: true)] #[Column(length: 255, nullable: true)]
private ?string $avatarPath = null; private ?string $avatarPath = null;
#[Column(nullable: true)]
private ?bool $consentGiven = null;
public function getId(): ?int public function getId(): ?int
{ {
@@ -243,4 +246,15 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TotpTwo
$this->backupCodes = $backupCodes; $this->backupCodes = $backupCodes;
return $this; return $this;
} }
public function isConsentGiven(): ?bool
{
return $this->consentGiven;
}
public function setConsentGiven(?bool $consentGiven): self
{
$this->consentGiven = $consentGiven;
return $this;
}
} }

View File

@@ -12,6 +12,7 @@ namespace App\Form;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
@@ -19,6 +20,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
@@ -68,6 +70,13 @@ class RegistrationFormType extends AbstractType
), ),
], ],
]) ])
->add('consentGiven', CheckboxType::class, [
'label' => 'I have read the Privacy and Data Processing Policy and I consent to the processing of my data.',
'mapped' => true,
'constraints' => [
new IsTrue(message: 'You must agree to the privacy policy to create an account.'),
],
])
->add('recaptcha', RecaptchaType::class); ->add('recaptcha', RecaptchaType::class);
} }

View File

@@ -10,6 +10,7 @@
namespace App\Interfaces; namespace App\Interfaces;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
/** /**
@@ -24,7 +25,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
*/ */
interface TopicManagerInterface interface TopicManagerInterface
{ {
public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user): void; public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user, Request $request): void;
public function unSubscribe(string $gameAssoc, string $userName): void; public function unSubscribe(string $gameAssoc, string $userName): void;

View File

@@ -0,0 +1,42 @@
<?php declare(strict_types=1);
/**
* This file is part of the SplendidBear Websites' projects.
*
* Copyright (c) 2026 @ www.splendidbear.org
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Class Version20260416094849
*
* @package App\Migrations
* @author Lang <https://www.splendidbear.org>
* @category Class
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
* @link www.splendidbear.org
* @since 2026. 04. 16.
*/
final class Version20260416094849 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add consent property to user entity for GDPR compliance';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE app_user ADD consent_given BOOLEAN DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE app_user DROP consent_given');
}
}

View File

@@ -18,14 +18,15 @@ use App\Entity\User;
use App\Interfaces\TopicManagerInterface; use App\Interfaces\TopicManagerInterface;
use App\Repository\PlayedGameRepository; use App\Repository\PlayedGameRepository;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use DateTimeInterface;
use DateTime; use DateTime;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exception; use Exception;
use JsonException; use JsonException;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use RuntimeException; use RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update; use Symfony\Component\Mercure\Update;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@@ -52,7 +53,7 @@ readonly class TopicManager implements TopicManagerInterface
) { ) {
} }
public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user): void public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user, Request $request): void
{ {
$playedGame = $this->getPlayedGame($gameAssoc); $playedGame = $this->getPlayedGame($gameAssoc);
if (null === $playedGame) { if (null === $playedGame) {
@@ -70,7 +71,7 @@ readonly class TopicManager implements TopicManagerInterface
/** Save the player to the database on a fresh join */ /** Save the player to the database on a fresh join */
if (!$isKnown && $count < 2) { if (!$isKnown && $count < 2) {
$users = $this->saveUserToDb($gameAssoc, $userName, $user, $count + 1); $users = $this->saveUserToDb($gameAssoc, $userName, $user, $count + 1, $request);
$count = $this->getPlayerCount($users); $count = $this->getPlayerCount($users);
} }
@@ -168,9 +169,6 @@ readonly class TopicManager implements TopicManagerInterface
return $data; return $data;
} }
// ------------------------------------------------------------------ //
// Normal move
// ------------------------------------------------------------------ //
$coords = $event['coords']; $coords = $event['coords'];
$player = $event['player']; // 'red' | 'blue' $player = $event['player']; // 'red' | 'blue'
$isBomb = (bool)$event['bomb']; $isBomb = (bool)$event['bomb'];
@@ -243,10 +241,6 @@ readonly class TopicManager implements TopicManagerInterface
return $data; return $data;
} }
// ------------------------------------------------------------------ //
// Grid helpers
// ------------------------------------------------------------------ //
/** Load the grid rows from the database as a 2-D array. */ /** Load the grid rows from the database as a 2-D array. */
private function loadGrid(string $gameAssoc): array private function loadGrid(string $gameAssoc): array
{ {
@@ -403,10 +397,6 @@ readonly class TopicManager implements TopicManagerInterface
return $mines; return $mines;
} }
// ------------------------------------------------------------------ //
// Database helpers
// ------------------------------------------------------------------ //
private function getPlayedGame(string $gameAssoc): ?PlayedGame private function getPlayedGame(string $gameAssoc): ?PlayedGame
{ {
return $this->playedGameRepository->findOneByGameAssoc($gameAssoc); return $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
@@ -462,13 +452,18 @@ readonly class TopicManager implements TopicManagerInterface
} }
} }
private function saveUserToDb(string $gameAssoc, string $userName, ?UserInterface $user, int $count): array private function saveUserToDb(
{ string $gameAssoc,
string $userName,
?UserInterface $user,
int $count,
Request $request
): array {
$playedGame = $this->getPlayedGame($gameAssoc); $playedGame = $this->getPlayedGame($gameAssoc);
null !== $user null !== $user
? $this->saveRegisteredUser($userName, $count, $playedGame) ? $this->saveRegisteredUser($userName, $count, $playedGame)
: $this->saveAnonUser($userName, $count, $playedGame); : $this->saveAnonUser($userName, $count, $playedGame, $request);
$this->entityManager->persist($playedGame); $this->entityManager->persist($playedGame);
$this->entityManager->flush(); $this->entityManager->flush();
@@ -495,11 +490,14 @@ readonly class TopicManager implements TopicManagerInterface
} }
} }
private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame): void private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame, Request $request): void
{ {
try { try {
$anon = new Gamer(); $anon = new Gamer();
$anon->setUsername($userName); $anon->setUserName($userName);
$anon->setIp($request->getClientIp());
$anon->setCountry($this->extractCountry($request));
$anon->setUserAgent($request->headers->get('User-Agent'));
$anon->setConnTimestamp(new DateTime()); $anon->setConnTimestamp(new DateTime());
$this->entityManager->persist($anon); $this->entityManager->persist($anon);
@@ -518,8 +516,8 @@ readonly class TopicManager implements TopicManagerInterface
private function getUserCollection(PlayedGame $playedGame): array private function getUserCollection(PlayedGame $playedGame): array
{ {
$redUser = $playedGame->getRed(); $redUser = $playedGame->getRed();
$blueUser = $playedGame->getBlue(); $blueUser = $playedGame->getBlue();
return [ return [
'red' => null !== $redUser ? $redUser->getUsername() : '', 'red' => null !== $redUser ? $redUser->getUsername() : '',
@@ -527,11 +525,11 @@ readonly class TopicManager implements TopicManagerInterface
'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '', 'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : '', 'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : '',
'redAvatar' => null !== $redUser && null !== $redUser->getAvatarPath() 'redAvatar' => null !== $redUser && null !== $redUser->getAvatarPath()
? $this->cacheManager->generateUrl($redUser->getAvatarPath(), 'avatar_thumb') ? $this->cacheManager->generateUrl($redUser->getAvatarPath(), 'avatar_thumb')
: null, : null,
'blueAvatar' => null !== $blueUser && null !== $blueUser->getAvatarPath() 'blueAvatar' => null !== $blueUser && null !== $blueUser->getAvatarPath()
? $this->cacheManager->generateUrl($blueUser->getAvatarPath(), 'avatar_thumb') ? $this->cacheManager->generateUrl($blueUser->getAvatarPath(), 'avatar_thumb')
: null, : null,
]; ];
} }
@@ -585,4 +583,27 @@ readonly class TopicManager implements TopicManagerInterface
$this->logger->error('Lobby publish error: ' . $e->getMessage()); $this->logger->error('Lobby publish error: ' . $e->getMessage());
} }
} }
private function extractCountry(Request $request): ?string
{
/** Common headers used by CDNs and proxies to pass country information */
$countryHeaders = [
'CF-IPCountry', // Cloudflare
'CloudFront-Viewer-Country', // AWS CloudFront
'X-Country-Code', // Custom header
'X-Geoip-Country', // Generic GeoIP header
];
foreach ($countryHeaders as $header) {
$country = $request->headers->get($header);
if (empty($country)) {
continue;
}
return substr($country, 0, 100);
}
return null;
}
} }

View File

@@ -3,7 +3,7 @@
{% block title %} - The Game{% endblock %} {% block title %} - The Game{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta property="og:url" content="{{ url('MineSeekerBundle_homepage') | replace({'http://': 'https://'}) }}"/> <meta property="og:url" content="{{ url('MineSeekerBundle_homepage') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta property="og:site_name" content="MineSeeker"/> <meta property="og:site_name" content="MineSeeker"/>
@@ -23,9 +23,7 @@
{% endblock %} {% endblock %}
{% block header %} {% block header %}
<section <section id="hero-auth">
class="hero{% if app.request.attributes.get('_route') != 'MineSeekerBundle_homepage' %} hero--compact{% endif %}">
<div class="hero-auth"> <div class="hero-auth">
{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %} {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
<a href="{{ path('MineSeekerBundle_profile') }}" class="hero-auth-btn hero-auth-btn--profile"> <a href="{{ path('MineSeekerBundle_profile') }}" class="hero-auth-btn hero-auth-btn--profile">
@@ -56,7 +54,10 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
</section>
<section
class="hero{% if app.request.attributes.get('_route') != 'MineSeekerBundle_homepage' %} hero--compact{% endif %}">
<a class="hero-logo" href="{{ path('MineSeekerBundle_homepage') }}"> <a class="hero-logo" href="{{ path('MineSeekerBundle_homepage') }}">
<img src="{{ asset('images/mine-logo-txt.png') }}" alt="MineSeeker"/> <img src="{{ asset('images/mine-logo-txt.png') }}" alt="MineSeeker"/>
</a> </a>
@@ -253,6 +254,7 @@
<p class="footer-nav-label">Navigate</p> <p class="footer-nav-label">Navigate</p>
<ul> <ul>
<li><a href="{{ path('MineSeekerBundle_homepage') }}">Homepage</a></li> <li><a href="{{ path('MineSeekerBundle_homepage') }}">Homepage</a></li>
<li><a href="{{ path('MineSeekerBundle_rules') }}">Game Rules</a></li>
<li><a href="{{ path('MineSeekerBundle_terms') }}">Terms of Use</a></li> <li><a href="{{ path('MineSeekerBundle_terms') }}">Terms of Use</a></li>
<li><a href="{{ path('MineSeekerBundle_privacy') }}">Privacy Policy</a></li> <li><a href="{{ path('MineSeekerBundle_privacy') }}">Privacy Policy</a></li>
<li><a href="{{ path('MineSeekerBundle_contact') }}">Contact</a></li> <li><a href="{{ path('MineSeekerBundle_contact') }}">Contact</a></li>

View File

@@ -23,7 +23,7 @@
<meta property="og:title" content="Your friend challenges YOU!"/> <meta property="og:title" content="Your friend challenges YOU!"/>
<meta property="og:description" content="Do you accept the challenge?"/> <meta property="og:description" content="Do you accept the challenge?"/>
<meta property="og:image" <meta property="og:image"
content="{{ app.request.getSchemeAndHttpHost() }}{{ asset('/images/mine-1600x627.png') }}"/> content="https://{{ app.request.host }}{{ asset('/images/mine-1600x627.png') }}"/>
{% endblock %} {% endblock %}
{% block stylesheets %} {% block stylesheets %}

View File

@@ -3,7 +3,7 @@
{% block title %} - Contact{% endblock %} {% block title %} - Contact{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta property="og:url" content="{{ url('MineSeekerBundle_contact') | replace({'http://': 'https://'}) }}"/> <meta property="og:url" content="{{ url('MineSeekerBundle_contact') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta property="og:site_name" content="MineSeeker"/> <meta property="og:site_name" content="MineSeeker"/>

View File

@@ -3,7 +3,7 @@
{% block title %} - Privacy Policy{% endblock %} {% block title %} - Privacy Policy{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta property="og:url" content="{{ url('MineSeekerBundle_privacy') | replace({'http://': 'https://'}) }}"/> <meta property="og:url" content="{{ url('MineSeekerBundle_privacy') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta property="og:site_name" content="MineSeeker"/> <meta property="og:site_name" content="MineSeeker"/>

View File

@@ -0,0 +1,144 @@
{% extends 'Game/index.html.twig' %}
{% block title %} - Game Rules{% endblock %}
{% block metas %}
{%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta property="og:url" content="{{ url('MineSeekerBundle_rules') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="website"/>
<meta property="og:site_name" content="MineSeeker"/>
<meta property="og:title" content="Game Rules · MineSeeker"/>
<meta property="og:description" content="Learn how to play MineSeeker and discover what you unlock by creating a free account."/>
<meta property="og:image" content="{{ _ogImage }}"/>
<meta property="og:image:width" content="1600"/>
<meta property="og:image:height" content="627"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" content="Game Rules · MineSeeker"/>
<meta name="twitter:description" content="Learn how to play MineSeeker and discover what you unlock by creating a free account."/>
<meta name="twitter:image" content="{{ _ogImage }}"/>
{% endblock %}
{% block body %}
<div class="txt">
<h2>MineSeeker Game Rules</h2>
<p>MineSeeker is a real-time 1v1 twist on the classic minesweeper formula. Two players &mdash; <strong>Red</strong> and <strong>Blue</strong> &mdash; race over the same hidden minefield, taking turns to <strong>hunt the mines</strong>. Each mine you detonate is claimed in your colour and scores a point. The first player to claim the majority of the mines wins.</p>
<h3>1. The Board</h3>
<ul>
<li>The playing field is a <strong>16&times;16 grid</strong> of covered cells.</li>
<li><strong>51 mines</strong> are hidden randomly across the board at the start of each match.</li>
<li>Every non-mine cell displays a number indicating how many of its eight neighbours contain a mine &mdash; these numbers are your clues. Cells with no adjacent mines are empty.</li>
</ul>
<h3>2. Turn Order</h3>
<ul>
<li>Players alternate turns. On your turn the status bar reads <em>&ldquo;It is your turn! Make a move&rdquo;</em>; while you wait it reads <em>&ldquo;Your buddy is making a move&rdquo;</em>.</li>
<li>On your turn you must perform exactly one action: reveal a cell, flag/unflag a cell, or deploy your bomb.</li>
<li><strong>If you hit a mine, you keep your turn</strong> and may click again. Your turn only ends when you reveal a safe cell (or use your bomb).</li>
</ul>
<h3>3. Revealing Cells</h3>
<ul>
<li><strong>Left-click</strong> a covered cell to reveal it.</li>
<li>Revealing a <strong>numbered cell</strong> just uncovers the clue &mdash; no points are awarded &mdash; and your turn ends.</li>
<li>Revealing an <strong>empty cell</strong> triggers a <strong>flood-fill</strong> that opens all connected empty cells and their numbered borders in a single move. Flood-fill will never step onto a mine, so empty-area sweeps are always safe.</li>
<li><strong>Right-click</strong> a covered cell to place a flag where you suspect a mine. Flagged cells cannot be revealed until unflagged.</li>
</ul>
<h3>4. Claiming Mines &amp; Scoring</h3>
<ul>
<li>Clicking a mine is the <strong>goal</strong> of the game, not a failure. The mine is marked with your colour&rsquo;s flag and scores <strong>one point</strong> for you.</li>
<li>You keep the turn and may click again &mdash; rack up a streak while you&rsquo;re hot.</li>
<li>Your turn only ends when you finally reveal a safe cell (or deploy your bomb).</li>
</ul>
<h3>5. The Bomb</h3>
<ul>
<li>Each player carries <strong>one bomb</strong> per match.</li>
<li>Detonating your bomb clears a <strong>5&times;5 blast radius</strong> (25 cells) around the target. <strong>Every mine inside the radius is claimed for your colour and adds to your score.</strong> Numbered cells in the radius are also revealed.</li>
<li>The bomb consumes your turn and can only be used once. Save it for a dense patch of suspected mines to burst ahead on the scoreboard.</li>
</ul>
<h3>6. Winning the Match</h3>
<p>A match ends in one of three ways:</p>
<ul>
<li><strong>Majority reached</strong> &mdash; the first player to claim more than half of the mines (26 of 51) wins immediately.</li>
<li><strong>A player resigns</strong> &mdash; the remaining player wins.</li>
<li><strong>Draw</strong> &mdash; if neither player reaches the majority and scores end up equal, the match is recorded as a draw.</li>
</ul>
<h3>7. Playing as a Guest</h3>
<p>No account is required to play. Just open the game, share the match link with a friend, and play. Guest matches are not saved to a history and carry no stats.</p>
<h2 style="margin-top: 40px;">Registered User Privileges</h2>
<p>Creating a free account unlocks everything the guest experience leaves behind. Registration takes under a minute and your email is only used for account recovery.</p>
<h3>1. Persistent Game History</h3>
<ul>
<li>Every match you play is recorded with timestamps, the full move list, the final grid, and your opponent&rsquo;s name.</li>
<li>Replay past battles cell-by-cell and share them with a public UUID link so friends can watch your finest detonations.</li>
</ul>
<div class="img-container">
<img style="margin-top: 15px;" src="{{ asset('images/privileges/history.png') }}" alt="Recent Game History" />
</div>
<h3>2. Player Statistics</h3>
<ul>
<li>Total games, wins, losses, and draws.</li>
<li>Win rate percentage, average score, personal best score, and total mines hit.</li>
<li>A 6-month trend dashboard charting wins, losses, and draws per month.</li>
</ul>
<div class="img-container">
<img style="margin-top: 15px;" src="{{ asset('images/privileges/stat.png') }}" alt="Statistics" />
</div>
<h3>3. Profile &amp; Identity</h3>
<ul>
<li>Upload a custom <strong>avatar</strong> that appears next to your username on the board and in the shared battles.</li>
<li>Your username is reserved &mdash; no one else can take it.</li>
</ul>
<h3>4. Account Security</h3>
<ul>
<li><strong>Two-factor authentication (TOTP):</strong> Protect your account with an authenticator app and a set of one-time backup codes.</li>
<li><strong>WebAuthn passkeys:</strong> Register one or more hardware/biometric security keys for passwordless sign-in.</li>
<li>A dedicated <strong>Security</strong> dashboard to manage backup codes, review registered credentials, and rotate them at any time.</li>
</ul>
<div class="img-container">
<img style="margin-top: 15px;" src="{{ asset('images/privileges/security.png') }}" alt="Security Dashboard" />
</div>
<h3>5. Shareable Battle Pages</h3>
<ul>
<li>Each recorded match gets a public page with both players&rsquo; names, avatars, final scores, the outcome, and a compact summary of how it played out.</li>
<li>Perfect for proving that impossible last-turn comeback.</li>
</ul>
<div class="img-container">
<img style="margin-top: 15px;" src="{{ asset('images/privileges/battle.png') }}" alt="Shareable Battle Pages" />
</div>
<div class="img-container">
<img style="margin-top: 15px;" src="{{ asset('images/privileges/shared-battle.png') }}" alt="Shared Battle Page" />
</div>
<p style="margin-top: 32px;">Ready to level up? <a href="{{ path('MineSeekerBundle_register') }}">Create your free account</a> or <a href="{{ path('MineSeekerBundle_gamePlay') }}">jump straight into a match</a>.</p>
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% block title %} - Terms of Service{% endblock %} {% block title %} - Terms of Service{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta property="og:url" content="{{ url('MineSeekerBundle_terms') | replace({'http://': 'https://'}) }}"/> <meta property="og:url" content="{{ url('MineSeekerBundle_terms') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta property="og:site_name" content="MineSeeker"/> <meta property="og:site_name" content="MineSeeker"/>

View File

@@ -3,7 +3,7 @@
{% block title %} - Forgot Password{% endblock %} {% block title %} - Forgot Password{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta name="robots" content="noindex,nofollow"/> <meta name="robots" content="noindex,nofollow"/>
<meta property="og:url" content="{{ app.request.uri }}"/> <meta property="og:url" content="{{ app.request.uri }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>

View File

@@ -3,7 +3,7 @@
{% block title %} - Sign In{% endblock %} {% block title %} - Sign In{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta name="robots" content="noindex,nofollow"/> <meta name="robots" content="noindex,nofollow"/>
<meta property="og:url" content="{{ app.request.uri }}"/> <meta property="og:url" content="{{ app.request.uri }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>

View File

@@ -3,7 +3,7 @@
{% block title %} - Profile{% endblock %} {% block title %} - Profile{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta name="robots" content="noindex,nofollow"/> <meta name="robots" content="noindex,nofollow"/>
<meta property="og:url" content="{{ url('MineSeekerBundle_profile') | replace({'http://': 'https://'}) }}"/> <meta property="og:url" content="{{ url('MineSeekerBundle_profile') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="profile"/> <meta property="og:type" content="profile"/>

View File

@@ -3,7 +3,7 @@
{% block title %} - Security Settings{% endblock %} {% block title %} - Security Settings{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta name="robots" content="noindex,nofollow"/> <meta name="robots" content="noindex,nofollow"/>
<meta property="og:url" content="{{ url('MineSeekerBundle_profile_security') | replace({'http://': 'https://'}) }}"/> <meta property="og:url" content="{{ url('MineSeekerBundle_profile_security') | replace({'http://': 'https://'}) }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>

View File

@@ -3,7 +3,7 @@
{% block title %} - Register{% endblock %} {% block title %} - Register{% endblock %}
{% block metas %} {% block metas %}
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} {%- set _ogImage = 'https://' ~ app.request.host ~ asset('/images/mine-1600x627.png') -%}
<meta property="og:url" content="{{ app.request.uri }}"/> <meta property="og:url" content="{{ app.request.uri }}"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta property="og:site_name" content="MineSeeker"/> <meta property="og:site_name" content="MineSeeker"/>
@@ -117,6 +117,25 @@
</div> </div>
</div> </div>
<div class="auth-field">
<label class="auth-checkbox-label" style="display: flex; align-items: flex-start; cursor: pointer; user-select: none;">
{{ form_widget(form.consentGiven, {
attr: {
class: 'auth-checkbox',
style: 'margin-right: 10px; margin-top: 3px;'
}
}) }}
<span style="flex: 1; font-size: 14px; line-height: 1.5; color: #666;">
I have read the <a href="{{ path('MineSeekerBundle_privacy') }}" target="_blank" style="color: #667eea; text-decoration: none;">Privacy and Data Processing Policy</a> and I consent to the processing of my data. *
</span>
</label>
{% if not form.consentGiven.vars.valid %}
{% for error in form.consentGiven.vars.errors %}
<p class="auth-field-error"><i class="fas fa-circle-exclamation"></i> {{ error.message }}</p>
{% endfor %}
{% endif %}
</div>
<button type="submit" class="auth-submit"> <button type="submit" class="auth-submit">
<i class="fas fa-user-plus"></i> Create Account <i class="fas fa-user-plus"></i> Create Account
</button> </button>