Compare commits
11 Commits
v2026.2.1-
...
v2026.2.3-
| Author | SHA1 | Date | |
|---|---|---|---|
| dc9c5f6545 | |||
| 25f2aaab8c | |||
| 0cc9cdaf07 | |||
| 247f437445 | |||
| 0e94367223 | |||
| a9ee28b395 | |||
| bd074c5c9d | |||
| 42c552c528 | |||
| 3b376e5386 | |||
| 45a8e6b4a1 | |||
| 1f8e9c3c56 |
32
.gitchangelog.rc
Normal file
32
.gitchangelog.rc
Normal 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 = []
|
||||
577
CHANGELOG.md
577
CHANGELOG.md
@@ -1,200 +1,403 @@
|
||||
Changelog
|
||||
=========
|
||||
# Changelog
|
||||
|
||||
|
||||
(unreleased)
|
||||
------------
|
||||
## v2026.2.2-9 (2026-04-18)
|
||||
|
||||
New
|
||||
~~~
|
||||
- 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]
|
||||
### New
|
||||
|
||||
Changes
|
||||
~~~~~~~
|
||||
- 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]
|
||||
* Add rules page #4. [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]
|
||||
### Fix
|
||||
|
||||
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]
|
||||
* The font-awesome simplifying to work on bare-metal - & fix all warnings at build time #4. [Lang]
|
||||
|
||||
* The css problem had been solved on reponsive gfx on homepage #4. [Lang]
|
||||
|
||||
|
||||
1.1.0 (2019-10-26)
|
||||
------------------
|
||||
## v2026.2.1-8 (2026-04-18)
|
||||
|
||||
Changes
|
||||
~~~~~~~
|
||||
- Reinit project - disable redis module and make the project compatible
|
||||
w/ PHP7.3 #2. [Lang]
|
||||
### Fix
|
||||
|
||||
* Quickfix for https-only login - & add user data when the user is not logged in #4. [Lang]
|
||||
|
||||
|
||||
0.4.0 (2019-10-26)
|
||||
------------------
|
||||
- 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]
|
||||
## v2026.2.1-7 (2026-04-16)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add consent checkbox to user's registration - and fix the sharing pics #4. [Lang]
|
||||
|
||||
* Add correct version numbering and CHANGELOG - and add the LICENSE #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-6 (2026-04-16)
|
||||
|
||||
### Changes
|
||||
|
||||
* Update all texts on all pages - extend them with the game specific things #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-5 (2026-04-16)
|
||||
|
||||
### Fix
|
||||
|
||||
* The meta tags does not have https scheme - nothing worked in configuration #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-4 (2026-04-15)
|
||||
|
||||
### New
|
||||
|
||||
* Add notification email when a user is registered #4. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Add notification on activation too #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-2 (2026-04-15)
|
||||
|
||||
### Fix
|
||||
|
||||
* Another attempt to fix the email assets #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-1 (2026-04-15)
|
||||
|
||||
### Fix
|
||||
|
||||
* The images does not shows in emails #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-0 (2026-04-15)
|
||||
|
||||
### New
|
||||
|
||||
* Add Contact page with email sending behaviour #4. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Add missing .env variable and increase the version number and add missing data from front-end and back-end deps descriptor #4. [Lang]
|
||||
|
||||
* Change the shareable battle - add avatars to it - even on the og tags #4. [Lang]
|
||||
|
||||
* Change text #4. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* The mailhog is crashed on development env #4. [Lang]
|
||||
|
||||
* The og tags did not have proper http schema - they should have https #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-5 (2026-04-14)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add donation button #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-4 (2026-04-14)
|
||||
|
||||
### New
|
||||
|
||||
* Add timer for the acceptance of the challenge #4. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* 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]
|
||||
|
||||
### Fix
|
||||
|
||||
* Missing font-awesome icons on bare-metal environment #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-3 (2026-04-14)
|
||||
|
||||
### Changes
|
||||
|
||||
* The user's avatar will be saved as a uuid.extension #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-1 (2026-04-14)
|
||||
|
||||
### Fix
|
||||
|
||||
* 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
124
LICENSE
Normal 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)
|
||||
|
||||
|
||||
33
Makefile
33
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt
|
||||
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
@@ -11,6 +11,9 @@ help:
|
||||
@echo " make down - Stop and remove containers/networks"
|
||||
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
|
||||
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
|
||||
@echo " make ccp - Clear the production cache"
|
||||
@echo " make cache-clear - Clear all caches (Vite, Symfony, node_modules)"
|
||||
@echo " make og-cache-clear - Clear Open Graph cache only"
|
||||
|
||||
start:
|
||||
docker compose up -d
|
||||
@@ -51,3 +54,31 @@ db-reset:
|
||||
bin/console doctrine:database:drop --force --if-exists --no-interaction
|
||||
bin/console doctrine:database:create --if-not-exists --no-interaction
|
||||
bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
ccp:
|
||||
bin/console cache:clear --no-warmup --env=prod
|
||||
|
||||
cache-clear:
|
||||
@echo "Clearing all caches..."
|
||||
@rm -rf node_modules/.vite
|
||||
@rm -rf .vite
|
||||
@rm -rf var/og-cache
|
||||
@php bin/console cache:clear --no-warmup
|
||||
@echo "✓ Vite cache cleared"
|
||||
@echo "✓ OG cache cleared"
|
||||
@echo "✓ Symfony cache cleared"
|
||||
@echo ""
|
||||
@echo "Rebuilding assets..."
|
||||
@bun run build
|
||||
@echo ""
|
||||
@echo "✓ All caches cleared and assets rebuilt!"
|
||||
@echo " Next step: Refresh browser with Ctrl+Shift+R"
|
||||
|
||||
og-cache-clear:
|
||||
@echo "Clearing Open Graph cache..."
|
||||
@rm -rf var/og-cache
|
||||
@echo "✓ OG cache cleared!"
|
||||
@echo " Battle card images will be regenerated on next access"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -287,6 +287,14 @@ git push origin v2026.01
|
||||
|
||||
---
|
||||
|
||||
## Game Documentation
|
||||
|
||||
For detailed information about game mechanics, bonus systems, and scoring rules, see the [docs](./docs/) directory:
|
||||
|
||||
- **[Bonus Points System](./docs/game-mechanics/BONUS_POINTS_SYSTEM.md)** — Complete reference for all bonus point types, calculation rules, and implementation details
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3.0 — see [LICENSE](LICENSE) for details.
|
||||
|
||||
@@ -7,9 +7,4 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
$font-path: "/build/webfonts";
|
||||
|
||||
@import '@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
@import '@fortawesome/fontawesome-free/scss/brands';
|
||||
@import '@fortawesome/fontawesome-free/scss/solid';
|
||||
@import '@fortawesome/fontawesome-free/scss/regular';
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
.hero-auth {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 36px;
|
||||
#hero-auth {
|
||||
padding: 20px;
|
||||
|
||||
.hero-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-auth-user {
|
||||
.hero-auth-user {
|
||||
font: 600 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.75);
|
||||
letter-spacing: 0.5px;
|
||||
@@ -17,6 +18,13 @@
|
||||
gap: 6px;
|
||||
|
||||
i { font-size: 15px; }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
.hero-auth {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-auth-btn {
|
||||
|
||||
@@ -32,4 +32,12 @@ main div.txt a {
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: #c5e8ff; }
|
||||
}
|
||||
}
|
||||
|
||||
main div.txt img {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
main div.txt .img-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ footer {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
// Left: brand block
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -55,7 +54,6 @@ footer {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// Right: navigation
|
||||
.footer-nav-label {
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
@@ -91,7 +89,6 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom copyright bar
|
||||
.footer-copy {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 16px 60px;
|
||||
@@ -112,4 +109,4 @@ footer {
|
||||
|
||||
&:hover { color: #95cff5; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,6 +859,104 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bshare-bonus {
|
||||
padding: 28px 28px 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.bshare-bonus__title {
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: #ffd700;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
i { font-size: 14px; }
|
||||
}
|
||||
|
||||
.bshare-bonus__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.bshare-bonus__player {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
&--red {
|
||||
border-color: rgba(246, 125, 82, 0.15);
|
||||
background: rgba(246, 125, 82, 0.04);
|
||||
}
|
||||
|
||||
&--blue {
|
||||
border-color: rgba(149, 207, 245, 0.15);
|
||||
background: rgba(149, 207, 245, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.bshare-bonus__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.bshare-bonus__points {
|
||||
font: 700 24px 'Rajdhani', sans-serif;
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.bshare-bonus__label {
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255, 215, 0, 0.7);
|
||||
}
|
||||
|
||||
.bshare-bonus__stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bshare-bonus__stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bshare-bonus__stat-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font: 500 11px 'Rajdhani', sans-serif;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.bshare-bonus__stat-value {
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 215, 0, 0.9);
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bshare-bonus__stat--empty {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bshare-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -97,4 +97,4 @@
|
||||
font-size: 20px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ main {
|
||||
}
|
||||
|
||||
.mine-container {
|
||||
background: url("/images/bg-mineseeker-0-outbg.jpg") no-repeat;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -54,4 +53,4 @@ main {
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
242
assets/css/mineseeker/_bonus-box.scss
Normal file
242
assets/css/mineseeker/_bonus-box.scss
Normal file
@@ -0,0 +1,242 @@
|
||||
/*!*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#mine-wrapper .bonus-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 58px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
background: #07090d;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
align-self: stretch;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box.red-bonus {
|
||||
background: linear-gradient(to bottom, #2a0502 0%, #4a1510 100%);
|
||||
border-color: rgba(246, 125, 82, 0.4);
|
||||
color: rgba(246, 125, 82, 0.85);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(246, 125, 82, 0.85);
|
||||
box-shadow: 0 0 12px rgba(173, 10, 5, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box.blue-bonus {
|
||||
background: linear-gradient(to bottom, #050f18 0%, #0f2838 100%);
|
||||
border-color: rgba(149, 207, 245, 0.4);
|
||||
color: rgba(149, 207, 245, 0.85);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(149, 207, 245, 0.85);
|
||||
box-shadow: 0 0 12px rgba(35, 111, 135, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box__icon {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box__value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.bsd {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
}
|
||||
|
||||
.bsd-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 22px 14px;
|
||||
border-bottom: 1px solid rgba(35, 111, 135, 0.3);
|
||||
}
|
||||
|
||||
.bsd-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bsd-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 2px;
|
||||
color: rgba(149, 207, 245, 0.7);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bsd-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
|
||||
.fa {
|
||||
color: #f6d572;
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-close {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(35, 111, 135, 0.4);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(149, 207, 245, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
padding: 18px 22px;
|
||||
}
|
||||
|
||||
.bsd-column {
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.bsd-column--red {
|
||||
border-color: rgba(246, 125, 82, 0.35);
|
||||
background: linear-gradient(to bottom, rgba(74, 6, 3, 0.35), rgba(107, 37, 21, 0.15));
|
||||
}
|
||||
|
||||
.bsd-column--blue {
|
||||
border-color: rgba(149, 207, 245, 0.35);
|
||||
background: linear-gradient(to bottom, rgba(11, 37, 48, 0.35), rgba(22, 61, 85, 0.15));
|
||||
}
|
||||
|
||||
.bsd-column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.bsd-column-name {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bsd-column-total {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #f6d572;
|
||||
|
||||
.fa {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-stats {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bsd-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 6px 2px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.06);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-stat-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bsd-stat-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.bsd-stat-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.bsd-stat-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bsd-note {
|
||||
margin: 0;
|
||||
padding: 12px 22px 18px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.bsd-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,6 @@
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field .field-corner {
|
||||
background: url('/images/bg-corner-outbg.png') no-repeat top left;
|
||||
background-size: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -206,4 +204,4 @@
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field img {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,6 @@ footer nav ul li {
|
||||
}
|
||||
|
||||
footer nav ul li:nth-child(even) {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -401,8 +400,4 @@ footer nav ul li a:hover {
|
||||
footer nav ul li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
footer nav ul li:nth-child(even) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
@import 'mineseeker/grid';
|
||||
@import 'mineseeker/back-button';
|
||||
@import 'mineseeker/timer';
|
||||
@import 'mineseeker/bonus-box';
|
||||
@import 'mineseeker/responsive';
|
||||
@import 'mineseeker/waiting-dialog';
|
||||
|
||||
@@ -50,7 +50,7 @@ const RESULT_META = {
|
||||
},
|
||||
};
|
||||
|
||||
function Avatar({ name, color, avatarUrl }) {
|
||||
function Avatar({ name, color, avatarUrl, bonusPoints = 0 }) {
|
||||
const isRed = 'red' === color;
|
||||
const initials = (name || '?').slice(0, 2).toUpperCase();
|
||||
|
||||
@@ -66,31 +66,53 @@ function Avatar({ name, color, avatarUrl }) {
|
||||
const textColor = isRed ? '#f67d52' : '#95cff5';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{
|
||||
width: 72, height: 72, borderRadius: '50%',
|
||||
background: avatarUrl ? 'transparent' : gradient,
|
||||
border: `2px solid ${border}`,
|
||||
boxShadow: glow,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
font: '800 24px \'Rajdhani\', sans-serif',
|
||||
color: textColor,
|
||||
letterSpacing: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
initials
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, position: 'relative' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{
|
||||
width: 72, height: 72, borderRadius: '50%',
|
||||
background: avatarUrl ? 'transparent' : gradient,
|
||||
border: `2px solid ${border}`,
|
||||
boxShadow: glow,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
font: '800 24px \'Rajdhani\', sans-serif',
|
||||
color: textColor,
|
||||
letterSpacing: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
initials
|
||||
)}
|
||||
</div>
|
||||
{0 < bonusPoints && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: -6,
|
||||
right: -6,
|
||||
background: '#ffd700',
|
||||
borderRadius: '50%',
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1)',
|
||||
border: '2px solid rgba(0,0,0,0.5)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-star" style={{ color: '#000', fontSize: 14 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span style={{
|
||||
@@ -210,13 +232,22 @@ export default function BattleDialog({ games }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="bd-vs-panel">
|
||||
<Avatar name={game.redName} color="red" avatarUrl={game.redAvatar} />
|
||||
<Avatar name={game.redName} color="red" avatarUrl={game.redAvatar} bonusPoints={game.redBonusPoints > game.blueBonusPoints ? game.redBonusPoints : 0} />
|
||||
<div className="bd-vs-center">
|
||||
<div className="bd-vs-score">
|
||||
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
||||
</div>
|
||||
<div className="bd-vs-score" style={{ marginBottom: 8 }}>
|
||||
<span style={{ font: '700 13px \'Rajdhani\', sans-serif', color: '#f67d52', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<i className="fa fa-star" style={{ fontSize: 11 }} /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span style={{ font: '700 13px \'Rajdhani\', sans-serif', color: '#95cff5', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" style={{ fontSize: 11 }} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="bd-vs-label">VS</div>
|
||||
<div
|
||||
className="bd-result-badge"
|
||||
@@ -225,7 +256,7 @@ export default function BattleDialog({ games }) {
|
||||
<i className={`fa ${meta.icon}`} /> {meta.label}
|
||||
</div>
|
||||
</div>
|
||||
<Avatar name={game.blueName} color="blue" avatarUrl={game.blueAvatar} />
|
||||
<Avatar name={game.blueName} color="blue" avatarUrl={game.blueAvatar} bonusPoints={game.blueBonusPoints > game.redBonusPoints ? game.blueBonusPoints : 0} />
|
||||
</div>
|
||||
<div className="bd-stats">
|
||||
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
|
||||
@@ -244,6 +275,62 @@ export default function BattleDialog({ games }) {
|
||||
<StatRow icon="fa-clock-o" label="Started" value={game.created} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(0 < game.redBonusPoints
|
||||
|| 0 < game.blueBonusPoints
|
||||
|| game.redBonusStats?.blindHits
|
||||
|| game.blueBonusStats?.blindHits
|
||||
) && (
|
||||
<div style={{ padding: '16px 20px 0', borderTop: '1px solid rgba(255,255,255,0.08)', marginTop: 16, marginBottom: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{/* Red Bonus */}
|
||||
<div style={{
|
||||
padding: 16,
|
||||
border: '1px solid rgba(173,10,5,0.2)',
|
||||
borderRadius: 6,
|
||||
background: 'rgba(173,10,5,0.05)',
|
||||
}}
|
||||
>
|
||||
<span style={{ font: '700 12px \'Rajdhani\', sans-serif', textTransform: 'uppercase', letterSpacing: 2, color: '#ffd700', display: 'block', marginBottom: 12 }}>
|
||||
<i className="fa fa-star" style={{ marginRight: 8 }} /> Red Bonus Statistics
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||
{0 < game.redBonusStats?.blindHits && <StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />}
|
||||
{0 < game.redBonusStats?.chainBest && <StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />}
|
||||
{0 < game.redBonusStats?.edgeMines && <StatRow icon="fa-border" label="Edge mines" value={game.redBonusStats.edgeMines} />}
|
||||
{0 < game.redBonusStats?.lastMineHits && <StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />}
|
||||
{0 < game.redBonusStats?.biggestReveal && <StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />}
|
||||
{!game.redBonusStats?.blindHits && !game.redBonusStats?.chainBest && !game.redBonusStats?.edgeMines && !game.redBonusStats?.lastMineHits && !game.redBonusStats?.biggestReveal
|
||||
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blue Bonus */}
|
||||
<div style={{
|
||||
padding: 16,
|
||||
border: '1px solid rgba(149,207,245,0.2)',
|
||||
borderRadius: 6,
|
||||
background: 'rgba(149,207,245,0.05)',
|
||||
}}
|
||||
>
|
||||
<span style={{ font: '700 12px \'Rajdhani\', sans-serif', textTransform: 'uppercase', letterSpacing: 2, color: '#ffd700', display: 'block', marginBottom: 12 }}>
|
||||
<i className="fa fa-star" style={{ marginRight: 8 }} /> Blue Bonus Statistics
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||
{0 < game.blueBonusStats?.blindHits && <StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />}
|
||||
{0 < game.blueBonusStats?.chainBest && <StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />}
|
||||
{0 < game.blueBonusStats?.edgeMines && <StatRow icon="fa-border" label="Edge mines" value={game.blueBonusStats.edgeMines} />}
|
||||
{0 < game.blueBonusStats?.lastMineHits && <StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />}
|
||||
{0 < game.blueBonusStats?.biggestReveal && <StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />}
|
||||
{!game.blueBonusStats?.blindHits && !game.blueBonusStats?.chainBest && !game.blueBonusStats?.edgeMines && !game.blueBonusStats?.lastMineHits && !game.blueBonusStats?.biggestReveal
|
||||
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
</ThemeProvider>
|
||||
|
||||
25
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
25
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const BonusBox = ({ color, points, onClick, title }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`bonus-box ${color}-bonus`}
|
||||
onClick={onClick}
|
||||
title={title || 'View bonus statistics'}
|
||||
aria-label={`${color} bonus points: ${points}`}
|
||||
>
|
||||
<i className="fa fa-star bonus-box__icon" />
|
||||
<span className="bonus-box__value">{points}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default BonusBox;
|
||||
97
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
97
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { BONUS_LABELS } from '@mine-utils';
|
||||
|
||||
const DIALOG_SX = {
|
||||
'& .MuiDialog-paper': {
|
||||
background: '#07090d',
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '46px 46px',
|
||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||
width: '560px',
|
||||
maxWidth: '94vw',
|
||||
overflow: 'hidden',
|
||||
color: '#fff',
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
background: 'rgba(2, 4, 8, 0.88)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
};
|
||||
|
||||
const formatPlayerName = name => {
|
||||
if (name && name.startsWith('anon_')) {
|
||||
return 'Anonymous';
|
||||
}
|
||||
|
||||
if (name && 10 < name.length) {
|
||||
return name.substring(0, 7) + '...';
|
||||
}
|
||||
|
||||
return name || 'Unknown';
|
||||
};
|
||||
|
||||
const PlayerColumn = ({ color, player }) => (
|
||||
<div className={`bsd-column bsd-column--${color}`}>
|
||||
<div className="bsd-column-header">
|
||||
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
||||
<span className="bsd-column-total">
|
||||
<i className="fa fa-star" />
|
||||
{player.bonusPoints}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="bsd-stats">
|
||||
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
||||
<li key={key} className="bsd-stat">
|
||||
<div className="bsd-stat-text">
|
||||
<span className="bsd-stat-label">{label}</span>
|
||||
<span className="bsd-stat-desc">{desc}</span>
|
||||
</div>
|
||||
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||
<Dialog open={open} onClose={onClose} sx={DIALOG_SX}>
|
||||
<div className="bsd">
|
||||
<div className="bsd-header">
|
||||
<div className="bsd-header-text">
|
||||
<span className="bsd-label">Scoring</span>
|
||||
<h2 className="bsd-title">
|
||||
<i className="fa fa-star" />
|
||||
Bonus Statistics
|
||||
</h2>
|
||||
</div>
|
||||
<button className="bsd-close" onClick={onClose} aria-label="Close">
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bsd-body">
|
||||
<PlayerColumn color="red" player={red} />
|
||||
<PlayerColumn color="blue" player={blue} />
|
||||
</div>
|
||||
<p className="bsd-note">
|
||||
Bonus points are awarded alongside the main score for skillful play.
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default BonusStatsDialog;
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import BonusBox from './BonusBox';
|
||||
import BonusStatsDialog from './BonusStatsDialog';
|
||||
|
||||
const renderAvatar = player => {
|
||||
if (!player.registered) return null;
|
||||
@@ -27,6 +29,7 @@ const GameTimer = () => {
|
||||
const [redTime, setRedTime] = useState(0);
|
||||
const [blueTime, setBlueTime] = useState(0);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||
const timerIntervalRef = useRef(null);
|
||||
const gameStartedRef = useRef(false);
|
||||
|
||||
@@ -160,8 +163,12 @@ const GameTimer = () => {
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const openBonusDialog = () => setBonusDialogOpen(true);
|
||||
const closeBonusDialog = () => setBonusDialogOpen(false);
|
||||
|
||||
return (
|
||||
<div className="game-timer-container">
|
||||
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={openBonusDialog} />
|
||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||
{renderAvatar(red)}
|
||||
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
@@ -172,6 +179,8 @@ const GameTimer = () => {
|
||||
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||
</div>
|
||||
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={openBonusDialog} />
|
||||
<BonusStatsDialog open={bonusDialogOpen} onClose={closeBonusDialog} red={red} blue={blue} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,7 +53,10 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
||||
/>
|
||||
)}
|
||||
<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) && (
|
||||
<div className="flag-mine">
|
||||
<img src={currentImage} alt="" />
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import User from './User';
|
||||
import BonusStatsDialog from '../BonusStatsDialog';
|
||||
|
||||
const UserControl = ({ resign }) => {
|
||||
const { webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle } = useGame();
|
||||
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||
const activeColor = activePlayer ? 'blue' : 'red';
|
||||
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
||||
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
||||
@@ -24,30 +26,44 @@ const UserControl = ({ resign }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBonusClick = () => {
|
||||
setBonusDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="users">
|
||||
<User
|
||||
color="blue" webPlayer={webPlayer} {...blue}
|
||||
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||
/>
|
||||
<div className="active-mines-container">
|
||||
<i className="fa fa-star" />
|
||||
<div className={minesClass}>
|
||||
<div className="active-mines-nbr">{mines}</div>
|
||||
<div className="active-mines-shine" />
|
||||
<Fragment>
|
||||
<div className="users">
|
||||
<User
|
||||
color="blue" webPlayer={webPlayer} {...blue}
|
||||
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||
onBonusClick={handleBonusClick}
|
||||
/>
|
||||
<div className="active-mines-container">
|
||||
<i className="fa fa-star" />
|
||||
<div className={minesClass}>
|
||||
<div className="active-mines-nbr">{mines}</div>
|
||||
<div className="active-mines-shine" />
|
||||
</div>
|
||||
<i className="fa fa-star" />
|
||||
</div>
|
||||
<i className="fa fa-star" />
|
||||
<div className="clear" />
|
||||
<User
|
||||
color="red" webPlayer={webPlayer} {...red}
|
||||
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||
onBonusClick={handleBonusClick}
|
||||
/>
|
||||
<button className={resignClass} onClick={resign}>
|
||||
<div className="resign-shine" />
|
||||
Resign
|
||||
</button>
|
||||
</div>
|
||||
<div className="clear" />
|
||||
<User
|
||||
color="red" webPlayer={webPlayer} {...red}
|
||||
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||
<BonusStatsDialog
|
||||
open={bonusDialogOpen}
|
||||
onClose={() => setBonusDialogOpen(false)}
|
||||
red={red}
|
||||
blue={blue}
|
||||
/>
|
||||
<button className={resignClass} onClick={resign}>
|
||||
<div className="resign-shine" />
|
||||
Resign
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,18 @@ export const GameProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
const applyStep = stepData => {
|
||||
const { player, bomb: isBomb, minesFound = 0, revealedCells = [], redPoints: rp, bluePoints: bp } = stepData;
|
||||
const {
|
||||
player,
|
||||
bomb: isBomb,
|
||||
minesFound = 0,
|
||||
revealedCells = [],
|
||||
redPoints: rp,
|
||||
bluePoints: bp,
|
||||
redBonusPoints = 0,
|
||||
blueBonusPoints = 0,
|
||||
redBonusStats = {},
|
||||
blueBonusStats = {},
|
||||
} = stepData;
|
||||
|
||||
if (isBomb) {
|
||||
sounds.current.bomb.play();
|
||||
@@ -176,6 +187,18 @@ export const GameProvider = ({ children }) => {
|
||||
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
||||
}
|
||||
|
||||
// Update bonus points and stats
|
||||
syncRed(p => ({
|
||||
...p,
|
||||
bonusPoints: 'red' === player ? redBonusPoints : p.bonusPoints,
|
||||
bonusStats: 'red' === player ? redBonusStats : p.bonusStats,
|
||||
}));
|
||||
syncBlue(p => ({
|
||||
...p,
|
||||
bonusPoints: 'blue' === player ? blueBonusPoints : p.bonusPoints,
|
||||
bonusStats: 'blue' === player ? blueBonusStats : p.bonusStats,
|
||||
}));
|
||||
|
||||
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
||||
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
||||
|
||||
|
||||
@@ -34,9 +34,23 @@ export const IMAGES = {
|
||||
bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
|
||||
};
|
||||
|
||||
export const BONUS_STATS_DEF = {
|
||||
blindHits: 0, chainBest: 0, chainCurrent: 0, lastMineHits: 0, edgeMines: 0, biggestReveal: 0,
|
||||
};
|
||||
|
||||
export const BONUS_LABELS = {
|
||||
blindHits: { label: 'Blind hits', desc: 'Mines clicked with no revealed number nearby' },
|
||||
chainBest: { label: 'Best chain', desc: 'Longest streak of consecutive mine-clicks' },
|
||||
chainCurrent: { label: 'Current chain', desc: 'Active consecutive mine-click streak' },
|
||||
lastMineHits: { label: 'Endgame mines', desc: 'Mines clicked while few remain on the board' },
|
||||
edgeMines: { label: 'Edge mines', desc: 'Mines clicked on the board boundary' },
|
||||
biggestReveal: { label: 'Biggest reveal', desc: 'Largest number of safe cells revealed in one click' },
|
||||
};
|
||||
|
||||
export const PLAYER_DEF = {
|
||||
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
||||
registered: false, avatar: null,
|
||||
bonusPoints: 0, bonusStats: { ...BONUS_STATS_DEF },
|
||||
};
|
||||
|
||||
export const DESC = {
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, bombRadius, initCells, patchCells } from './constants';
|
||||
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, BONUS_STATS_DEF, BONUS_LABELS, bombRadius, initCells, patchCells } from './constants';
|
||||
|
||||
@@ -33,7 +33,11 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
MINIO_ENDPOINT: http://minio: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:
|
||||
- app_var:/app/var
|
||||
- caddy_data:/data
|
||||
|
||||
@@ -23,12 +23,14 @@ security:
|
||||
auth_code_parameter_name: _auth_code
|
||||
post_only: true
|
||||
default_target_path: MineSeekerBundle_homepage
|
||||
always_use_default_target_path: false
|
||||
prepare_on_login: true
|
||||
prepare_on_access_denied: true
|
||||
form_login:
|
||||
login_path: MineSeekerBundle_login
|
||||
check_path: MineSeekerBundle_login
|
||||
default_target_path: MineSeekerBundle_homepage
|
||||
always_use_default_target_path: false
|
||||
username_parameter: _username
|
||||
password_parameter: _password
|
||||
enable_csrf: true
|
||||
|
||||
47
docs/README.md
Normal file
47
docs/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Mine-Seeker Game Documentation
|
||||
|
||||
This directory contains comprehensive documentation about the Mine-Seeker game mechanics and implementation.
|
||||
|
||||
## Game Mechanics
|
||||
|
||||
### [Bonus Points System](./game-mechanics/BONUS_POINTS_SYSTEM.md)
|
||||
Complete reference for the bonus points system including:
|
||||
- All 6 bonus point types (Blind Hit, Chain Combo, Edge Mine, Endgame Mine, Safe Cell Bonus, Biggest Reveal)
|
||||
- Calculation rules and examples
|
||||
- Bonus statistics tracking
|
||||
- Player name formatting in dialogs
|
||||
- Database schema
|
||||
- Implementation notes
|
||||
- Testing checklist
|
||||
|
||||
**Recommended for**: Developers working on bonus system, AI assistants implementing or debugging bonus features, understanding game scoring mechanics.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Bonus Points at a Glance
|
||||
| Bonus Type | Points | Condition |
|
||||
|-----------|--------|-----------|
|
||||
| Blind Hit | +2 | Mine with no revealed numbered neighbors |
|
||||
| Edge Mine | +1 | Mine on board boundary (row/col 0 or 15) |
|
||||
| Endgame Mine | +3 | Mine clicked when ≤10 mines remain |
|
||||
| Safe Cell | +0.5 each | ≥2 safe cells revealed (min requirement) |
|
||||
| Chain Combo | Tracked | Consecutive mine clicks (no safe clicks) |
|
||||
| Biggest Reveal | Tracked | Largest number of safe cells revealed |
|
||||
|
||||
### Key Rules
|
||||
- Safe cell bonus only awarded for ≥2 cells minimum
|
||||
- Chain counter resets on any safe cell click
|
||||
- Endgame threshold: 51 - (redPoints + bluePoints) ≤ 10
|
||||
- Bonus stats are per-player and persist in database
|
||||
|
||||
---
|
||||
|
||||
## Files Using This Information
|
||||
- Backend: `/src/Util/TopicManager.php`
|
||||
- Frontend: `/assets/js/mine-seeker/contexts/GameProvider.jsx`
|
||||
- UI: `/assets/js/mine-seeker/components/BonusStatsDialog.jsx`
|
||||
- Constants: `/assets/js/mine-seeker/utils/constants.jsx`
|
||||
|
||||
|
||||
168
docs/game-mechanics/BONUS_POINTS_SYSTEM.md
Normal file
168
docs/game-mechanics/BONUS_POINTS_SYSTEM.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Mine-Seeker Bonus Points System
|
||||
|
||||
## Overview
|
||||
The Mine-Seeker game includes a bonus points system that rewards skilled play. Bonus points are tracked separately from the main score and displayed in the "Bonus Statistics" dialog.
|
||||
|
||||
## Bonus Point Types
|
||||
|
||||
### 1. Blind Hit (+2 points)
|
||||
**When**: Click a mine with no revealed numbered neighbors around it.
|
||||
|
||||
**Example**: Mine surrounded by unrevealed cells = +2 points
|
||||
|
||||
---
|
||||
|
||||
### 2. Chain Combo
|
||||
**When**: Click consecutive mines without clicking any safe cell in between.
|
||||
|
||||
**Tracked as**:
|
||||
- `chainCurrent`: Current streak (resets when you click a safe cell)
|
||||
- `chainBest`: Longest streak achieved
|
||||
|
||||
**Example**: Mine → Mine → Mine = chainBest becomes 3
|
||||
|
||||
---
|
||||
|
||||
### 3. Edge Mine (+1 point)
|
||||
**When**: Click a mine on the board boundary (row 0, row 15, col 0, or col 15).
|
||||
|
||||
**Example**: Click a mine on the edge = +1 point
|
||||
|
||||
---
|
||||
|
||||
### 4. Endgame Mine (+3 points)
|
||||
**When**: Click a mine when 10 or fewer mines remain on the board.
|
||||
|
||||
**Calculation**: `51 total mines - (red_points + blue_points) = mines_remaining`
|
||||
|
||||
**Example**: When 8 mines remain, click one = +3 points
|
||||
|
||||
---
|
||||
|
||||
### 5. Safe Cell Bonus (+0.5 points per cell)
|
||||
**When**: Click a safe cell and reveal 2 or more cells.
|
||||
|
||||
**Important**: Minimum 2 cells required. Single cell reveals = 0 points.
|
||||
|
||||
**Examples**:
|
||||
- Reveal 1 safe cell = 0 points
|
||||
- Reveal 2 safe cells = 1.0 points
|
||||
- Reveal 11 safe cells = 5.5 points
|
||||
|
||||
---
|
||||
|
||||
### 6. Biggest Reveal (Tracking stat)
|
||||
**What**: Tracks the largest number of safe cells revealed in one click.
|
||||
|
||||
**Example**: Largest reveal in a game = 15 cells shown in stats
|
||||
|
||||
---
|
||||
|
||||
## Bonus Statistics Display
|
||||
|
||||
### Dialog Shows
|
||||
- Both players' bonus statistics side-by-side
|
||||
- Each stat with label, description, and value
|
||||
- Total bonus points per player
|
||||
|
||||
### Player Name Rules
|
||||
- `anon_*` usernames → displays as "Anonymous"
|
||||
- Names longer than 10 chars → truncated to 7 chars + "..." (example: `VeryLongName` → `VeryLon...`)
|
||||
|
||||
---
|
||||
|
||||
## Tracked Statistics
|
||||
|
||||
```javascript
|
||||
{
|
||||
blindHits: 0, // Blind hit mines clicked
|
||||
chainBest: 0, // Longest mine streak
|
||||
chainCurrent: 0, // Current active streak
|
||||
lastMineHits: 0, // Endgame mines clicked
|
||||
edgeMines: 0, // Edge mine clicks
|
||||
biggestReveal: 0 // Largest safe cell reveal
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
- `red_bonus_points` (FLOAT) — Red player's total bonus points
|
||||
- `blue_bonus_points` (FLOAT) — Blue player's total bonus points
|
||||
- `red_bonus_stats` (JSON) — Red player's tracked stats
|
||||
- `blue_bonus_stats` (JSON) — Blue player's tracked stats
|
||||
|
||||
---
|
||||
|
||||
## 📋 Documentation Maintenance
|
||||
|
||||
**IMPORTANT**: This documentation must be updated whenever:
|
||||
- New bonus types are added
|
||||
- Point values change
|
||||
- Bonus calculation logic changes
|
||||
- New stats are tracked
|
||||
- Bonus display rules change
|
||||
|
||||
**Update these files**:
|
||||
1. This file (`BONUS_POINTS_SYSTEM.md`) — Update descriptions and examples
|
||||
2. Code comments in `/src/Util/TopicManager.php` — Explain calculation logic
|
||||
3. `/docs/README.md` — Update Quick Reference table if values change
|
||||
|
||||
**Keep documentation**:
|
||||
- ✅ Simple and clear
|
||||
- ✅ With real code examples
|
||||
- ✅ Synchronized with actual code behavior
|
||||
- ✅ Updated before/after feature changes
|
||||
|
||||
---
|
||||
|
||||
## Implementation Files
|
||||
- Backend: `/src/Util/TopicManager.php` — Bonus calculation logic
|
||||
- Frontend: `/assets/js/mine-seeker/contexts/GameProvider.jsx` — State sync
|
||||
- UI: `/assets/js/mine-seeker/components/BonusStatsDialog.jsx` — Display dialog
|
||||
- Constants: `/assets/js/mine-seeker/utils/constants.jsx` — Labels and defaults
|
||||
|
||||
---
|
||||
|
||||
## Battle Report Display Components
|
||||
|
||||
**IMPORTANT**: The Bonus Statistics display appears in **two places** that must be kept in sync:
|
||||
|
||||
### 1. Public Battle Share Page
|
||||
**File**: `/templates/Game/battle_share.html.twig`
|
||||
- Displays via `bshare-bonus` CSS classes
|
||||
- Backend data passed from `ProfileController::battleShare()`
|
||||
- Shows bonus stats as HTML table format
|
||||
|
||||
### 2. Profile Dialog (BattleDialog component)
|
||||
**File**: `/assets/js/components/BattleDialog.jsx`
|
||||
- React component using Material-UI Dialog
|
||||
- Displays inside the match details modal on profile page
|
||||
- Shows bonus stats using `StatRow` components in side-by-side boxes
|
||||
|
||||
### Synchronization Requirements
|
||||
|
||||
When making changes to the bonus statistics display, update **BOTH** files:
|
||||
|
||||
1. **Update logic/data** → Edit both template and component
|
||||
2. **Change stat names** → Update both BONUS_LABELS and both display files
|
||||
3. **Modify formatting** → Keep visual consistency between both displays
|
||||
4. **Add new stats** → Add to both the `.twig` template AND the `.jsx` component
|
||||
|
||||
**Checklist for changes**:
|
||||
- [ ] Update `/src/Util/TopicManager.php` if bonus calculation changes
|
||||
- [ ] Update `/templates/Game/battle_share.html.twig` for public display
|
||||
- [ ] Update `/assets/js/components/BattleDialog.jsx` for profile dialog
|
||||
- [ ] Update `/assets/js/mine-seeker/utils/constants.jsx` if adding new stats
|
||||
- [ ] Test both displays show identical data
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist for Changes
|
||||
- [ ] Code changes implemented
|
||||
- [ ] This documentation updated
|
||||
- [ ] `/docs/README.md` Quick Reference table updated
|
||||
- [ ] Code comments added/updated
|
||||
- [ ] Examples updated to match new behavior
|
||||
- [ ] Both battle report displays tested
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"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/"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/images/privileges/battle.png
Normal file
BIN
public/images/privileges/battle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
public/images/privileges/history.png
Normal file
BIN
public/images/privileges/history.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/privileges/security.png
Normal file
BIN
public/images/privileges/security.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
public/images/privileges/shared-battle.png
Normal file
BIN
public/images/privileges/shared-battle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/privileges/stat.png
Normal file
BIN
public/images/privileges/stat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -111,6 +111,12 @@ class GameController extends AbstractController
|
||||
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
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -64,7 +64,7 @@ class MercureController extends AbstractController
|
||||
#[Route('/api/game/join/{gameAssoc}', name: 'MineSeekerBundle_api_game_join', methods: ['POST'])]
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -163,6 +163,10 @@ class ProfileController extends AbstractController
|
||||
'result' => $result,
|
||||
'myPoints' => $myPts,
|
||||
'oppPoints' => $oppPts,
|
||||
'redBonusPoints' => $game->getRedBonusPoints() ?? 0,
|
||||
'blueBonusPoints' => $game->getBlueBonusPoints() ?? 0,
|
||||
'redBonusStats' => $game->getRedBonusStats() ?? [],
|
||||
'blueBonusStats' => $game->getBlueBonusStats() ?? [],
|
||||
];
|
||||
}, $recent),
|
||||
'chartData' => [
|
||||
@@ -197,6 +201,10 @@ class ProfileController extends AbstractController
|
||||
$resign = $game->getResign();
|
||||
$redAvatar = $game->getRed()?->getAvatarPath();
|
||||
$blueAvatar = $game->getBlue()?->getAvatarPath();
|
||||
$redBonusPoints = $game->getRedBonusPoints() ?? 0;
|
||||
$blueBonusPoints = $game->getBlueBonusPoints() ?? 0;
|
||||
$redBonusStats = $game->getRedBonusStats() ?? [];
|
||||
$blueBonusStats = $game->getBlueBonusStats() ?? [];
|
||||
|
||||
if ($resign === 'red') {
|
||||
$summary = "$redName resigned — $blueName wins";
|
||||
@@ -215,16 +223,20 @@ class ProfileController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('Game/battle_share.html.twig', [
|
||||
'game' => $game,
|
||||
'redName' => $redName,
|
||||
'blueName' => $blueName,
|
||||
'redPts' => $redPts,
|
||||
'bluePts' => $bluePts,
|
||||
'resign' => $resign,
|
||||
'redAvatar' => $redAvatar,
|
||||
'blueAvatar' => $blueAvatar,
|
||||
'ogTitle' => "MineSeeker · $summary",
|
||||
'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.",
|
||||
'game' => $game,
|
||||
'redName' => $redName,
|
||||
'blueName' => $blueName,
|
||||
'redPts' => $redPts,
|
||||
'bluePts' => $bluePts,
|
||||
'resign' => $resign,
|
||||
'redAvatar' => $redAvatar,
|
||||
'blueAvatar' => $blueAvatar,
|
||||
'redBonusPoints' => $redBonusPoints,
|
||||
'blueBonusPoints' => $blueBonusPoints,
|
||||
'redBonusStats' => $redBonusStats,
|
||||
'blueBonusStats' => $blueBonusStats,
|
||||
'ogTitle' => "MineSeeker · $summary",
|
||||
'ogDesc' => "Watch the battle replay: $summary — played on MineSeeker, the multiplayer minesweeper.",
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Form\ResetPasswordFormType;
|
||||
use App\Repository\UserRepository;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use LogicException;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@@ -64,7 +65,7 @@ class SecurityController extends AbstractController
|
||||
#[Route('/logout', name: 'MineSeekerBundle_logout', methods: ['POST'])]
|
||||
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')]
|
||||
|
||||
@@ -62,6 +62,18 @@ class PlayedGame
|
||||
#[Column(length: 7, nullable: true)]
|
||||
private ?string $resign = null;
|
||||
|
||||
#[Column(nullable: true)]
|
||||
private ?float $redBonusPoints = null;
|
||||
|
||||
#[Column(nullable: true)]
|
||||
private ?float $blueBonusPoints = null;
|
||||
|
||||
#[Column(nullable: true)]
|
||||
private ?array $redBonusStats = null;
|
||||
|
||||
#[Column(nullable: true)]
|
||||
private ?array $blueBonusStats = null;
|
||||
|
||||
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||
private ?DateTime $created = null;
|
||||
|
||||
@@ -222,6 +234,46 @@ class PlayedGame
|
||||
$this->resign = $resign;
|
||||
}
|
||||
|
||||
public function getRedBonusPoints(): ?float
|
||||
{
|
||||
return $this->redBonusPoints;
|
||||
}
|
||||
|
||||
public function setRedBonusPoints(?float $redBonusPoints): void
|
||||
{
|
||||
$this->redBonusPoints = $redBonusPoints;
|
||||
}
|
||||
|
||||
public function getBlueBonusPoints(): ?float
|
||||
{
|
||||
return $this->blueBonusPoints;
|
||||
}
|
||||
|
||||
public function setBlueBonusPoints(?float $blueBonusPoints): void
|
||||
{
|
||||
$this->blueBonusPoints = $blueBonusPoints;
|
||||
}
|
||||
|
||||
public function getRedBonusStats(): ?array
|
||||
{
|
||||
return $this->redBonusStats;
|
||||
}
|
||||
|
||||
public function setRedBonusStats(?array $redBonusStats): void
|
||||
{
|
||||
$this->redBonusStats = $redBonusStats;
|
||||
}
|
||||
|
||||
public function getBlueBonusStats(): ?array
|
||||
{
|
||||
return $this->blueBonusStats;
|
||||
}
|
||||
|
||||
public function setBlueBonusStats(?array $blueBonusStats): void
|
||||
{
|
||||
$this->blueBonusStats = $blueBonusStats;
|
||||
}
|
||||
|
||||
public function getCreated(): ?DateTime
|
||||
{
|
||||
return $this->created;
|
||||
@@ -247,3 +299,5 @@ class PlayedGame
|
||||
return $this->steps;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -78,6 +78,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TotpTwo
|
||||
#[Column(length: 255, nullable: true)]
|
||||
private ?string $avatarPath = null;
|
||||
|
||||
#[Column(nullable: true)]
|
||||
private ?bool $consentGiven = null;
|
||||
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -243,4 +246,15 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TotpTwo
|
||||
$this->backupCodes = $backupCodes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isConsentGiven(): ?bool
|
||||
{
|
||||
return $this->consentGiven;
|
||||
}
|
||||
|
||||
public function setConsentGiven(?bool $consentGiven): self
|
||||
{
|
||||
$this->consentGiven = $consentGiven;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
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\PasswordType;
|
||||
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\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Email;
|
||||
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
namespace App\Interfaces;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
*/
|
||||
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;
|
||||
|
||||
|
||||
42
src/Migrations/2026/04/Version20260416094849.php
Normal file
42
src/Migrations/2026/04/Version20260416094849.php
Normal 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');
|
||||
}
|
||||
}
|
||||
48
src/Migrations/2026/04/Version20260418104430.php
Normal file
48
src/Migrations/2026/04/Version20260418104430.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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 Version20260418104430
|
||||
*
|
||||
* @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. 18.
|
||||
*/
|
||||
final class Version20260418104430 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add bonus stats to the playing experience';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE played_game ADD red_bonus_points DOUBLE PRECISION DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE played_game ADD blue_bonus_points DOUBLE PRECISION DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE played_game ADD red_bonus_stats JSON DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE played_game ADD blue_bonus_stats JSON DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE played_game DROP red_bonus_points');
|
||||
$this->addSql('ALTER TABLE played_game DROP blue_bonus_points');
|
||||
$this->addSql('ALTER TABLE played_game DROP red_bonus_stats');
|
||||
$this->addSql('ALTER TABLE played_game DROP blue_bonus_stats');
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,9 @@ class BattleCardGenerator
|
||||
{
|
||||
$path = $this->cachePath((int)$game->getId());
|
||||
|
||||
// Always regenerate to ensure bonus points are included
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
unlink($path);
|
||||
}
|
||||
|
||||
if (!is_dir($this->cacheDir)) {
|
||||
@@ -154,6 +155,12 @@ class BattleCardGenerator
|
||||
$scoreText = $redPts !== null && $bluePts !== null ? $redPts . ' : ' . $bluePts : 'VS';
|
||||
$this->centeredText($im, $scoreText, 72, self::WIDTH / 2, 390, $white);
|
||||
|
||||
/** Bonus points below score*/
|
||||
$redBonusPoints = $game->getRedBonusPoints() ?? 0;
|
||||
$blueBonusPoints = $game->getBlueBonusPoints() ?? 0;
|
||||
$bonusText = number_format((float)$redBonusPoints, 1, '.', '') . ' * : * ' . number_format((float)$blueBonusPoints, 1, '.', '');
|
||||
$this->centeredText($im, $bonusText, 24, self::WIDTH / 2, 425, $gold);
|
||||
|
||||
if ($winner === 'red') {
|
||||
$resultText = $redName . ' wins';
|
||||
$resultColor = $gold;
|
||||
@@ -169,11 +176,11 @@ class BattleCardGenerator
|
||||
}
|
||||
|
||||
if ($resultText !== '') {
|
||||
$this->centeredText($im, $resultText, 30, self::WIDTH / 2, 460, $resultColor);
|
||||
$this->centeredText($im, $resultText, 30, self::WIDTH / 2, 475, $resultColor);
|
||||
}
|
||||
|
||||
if ($resign) {
|
||||
$this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 498, $muted);
|
||||
$this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::WIDTH / 2, 508, $muted);
|
||||
}
|
||||
|
||||
$this->centeredText($im, 'mineseeker.hu', 16, self::WIDTH / 2, self::HEIGHT - 20, $muted);
|
||||
|
||||
@@ -18,14 +18,15 @@ use App\Entity\User;
|
||||
use App\Interfaces\TopicManagerInterface;
|
||||
use App\Repository\PlayedGameRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||
use DateTimeInterface;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use JsonException;
|
||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
@@ -43,16 +44,16 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
readonly class TopicManager implements TopicManagerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private HubInterface $hub,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
private CacheManager $cacheManager,
|
||||
private PlayedGameRepository $playedGameRepository,
|
||||
private UserRepository $userRepository,
|
||||
private CacheManager $cacheManager,
|
||||
) {
|
||||
}
|
||||
|
||||
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);
|
||||
if (null === $playedGame) {
|
||||
@@ -70,7 +71,7 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
|
||||
/** Save the player to the database on a fresh join */
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -95,8 +96,8 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
if ($count === 1) {
|
||||
// One player waiting — mark as active and announce to the lobby
|
||||
$playedGame->setUpdated(new DateTime());
|
||||
$this->entityManager->persist($playedGame);
|
||||
$this->entityManager->flush();
|
||||
$this->em->persist($playedGame);
|
||||
$this->em->flush();
|
||||
|
||||
$displayName = $users['red'] ?: $users['redAnon'] ?: $users['blue'] ?: $users['blueAnon'] ?: 'Unknown';
|
||||
$this->publishToLobby([
|
||||
@@ -120,8 +121,8 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
$users = $this->getUserCollection($playedGame);
|
||||
if ($this->getPlayerCount($users) === 1) {
|
||||
$playedGame->setUpdated(new DateTime('2000-01-01 00:00:00'));
|
||||
$this->entityManager->persist($playedGame);
|
||||
$this->entityManager->flush();
|
||||
$this->em->persist($playedGame);
|
||||
$this->em->flush();
|
||||
$this->publishToLobby(['action' => 'leave', 'gameAssoc' => $gameAssoc]);
|
||||
}
|
||||
}
|
||||
@@ -168,9 +169,6 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
return $data;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Normal move
|
||||
// ------------------------------------------------------------------ //
|
||||
$coords = $event['coords'];
|
||||
$player = $event['player']; // 'red' | 'blue'
|
||||
$isBomb = (bool)$event['bomb'];
|
||||
@@ -178,25 +176,40 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||
$grid = $this->loadGrid($gameAssoc);
|
||||
|
||||
// Cells already revealed by previous steps (as "row,col" => true map)
|
||||
/** Cells already revealed by previous steps (as "row,col" => true map) */
|
||||
$alreadyRevealed = $this->buildRevealedMap($playedGame);
|
||||
|
||||
// Determine which cells to reveal for this step
|
||||
/** Determine which cells to reveal for this step */
|
||||
if ($isBomb) {
|
||||
$revealedCells = $this->getBombRevealedCells($grid, $coords[0], $coords[1], $alreadyRevealed);
|
||||
} elseif ('m' === ($grid[$coords[0]][$coords[1]] ?? null)) {
|
||||
// Direct click on a mine — reveal it immediately (flood-fill skips mines)
|
||||
/** Direct click on a mine — reveal it immediately (flood-fill skips mines) */
|
||||
$revealedCells = [['row' => $coords[0], 'col' => $coords[1], 'value' => 'm']];
|
||||
} else {
|
||||
$revealedCells = $this->floodFill($grid, $coords[0], $coords[1], $alreadyRevealed);
|
||||
}
|
||||
|
||||
$minesFound = count(array_filter($revealedCells, static fn($c) => 'm' === $c['value']));
|
||||
$safeCellsFound = count(array_filter($revealedCells, static fn($c) => 'm' !== $c['value']));
|
||||
|
||||
$redPoints = ($playedGame->getRedPoints() ?? 0) + ('red' === $player ? $minesFound : 0);
|
||||
$bluePoints = ($playedGame->getBluePoints() ?? 0) + ('blue' === $player ? $minesFound : 0);
|
||||
$gameOver = $redPoints > 25 || $bluePoints > 25;
|
||||
|
||||
// Reveal remaining mines when the game ends
|
||||
/** Calculate bonus points and stats */
|
||||
$bonusData = $this->calculateBonuses(
|
||||
$playedGame,
|
||||
$player,
|
||||
$coords,
|
||||
$grid,
|
||||
$alreadyRevealed,
|
||||
$minesFound,
|
||||
$safeCellsFound,
|
||||
$redPoints,
|
||||
$bluePoints
|
||||
);
|
||||
|
||||
/** Reveal remaining mines when the game ends */
|
||||
$leftMines = [];
|
||||
if ($gameOver) {
|
||||
$finalRevealed = $alreadyRevealed;
|
||||
@@ -206,23 +219,27 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
$leftMines = $this->getLeftMines($grid, $finalRevealed);
|
||||
}
|
||||
|
||||
$this->saveStepToDb($gameAssoc, $event, $player, $revealedCells, $redPoints, $bluePoints);
|
||||
$this->saveStepToDb($gameAssoc, $event, $player, $revealedCells, $redPoints, $bluePoints, $bonusData);
|
||||
|
||||
$users = $this->getUserCollection($playedGame);
|
||||
$count = $this->getPlayerCount($users);
|
||||
$topic = 'mineseeker/channel/' . $gameAssoc;
|
||||
|
||||
$data = [
|
||||
'coords' => $coords,
|
||||
'player' => $player,
|
||||
'bomb' => $isBomb,
|
||||
'revealedCells' => $revealedCells,
|
||||
'minesFound' => $minesFound,
|
||||
'redPoints' => $redPoints,
|
||||
'bluePoints' => $bluePoints,
|
||||
'resign' => null,
|
||||
'gameOver' => $gameOver,
|
||||
'leftMines' => $leftMines,
|
||||
'coords' => $coords,
|
||||
'player' => $player,
|
||||
'bomb' => $isBomb,
|
||||
'revealedCells' => $revealedCells,
|
||||
'minesFound' => $minesFound,
|
||||
'redPoints' => $redPoints,
|
||||
'bluePoints' => $bluePoints,
|
||||
'resign' => null,
|
||||
'gameOver' => $gameOver,
|
||||
'leftMines' => $leftMines,
|
||||
'redBonusPoints' => $bonusData['redBonusPoints'],
|
||||
'blueBonusPoints' => $bonusData['blueBonusPoints'],
|
||||
'redBonusStats' => $bonusData['redBonusStats'],
|
||||
'blueBonusStats' => $bonusData['blueBonusStats'],
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -243,10 +260,6 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
return $data;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Grid helpers
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
/** Load the grid rows from the database as a 2-D array. */
|
||||
private function loadGrid(string $gameAssoc): array
|
||||
{
|
||||
@@ -266,6 +279,155 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
return $grid;
|
||||
}
|
||||
|
||||
private function calculateBonuses(
|
||||
PlayedGame $playedGame,
|
||||
string $player,
|
||||
array $coords,
|
||||
array $grid,
|
||||
array $alreadyRevealed,
|
||||
int $minesFound,
|
||||
int $safeCellsFound,
|
||||
int $redPoints,
|
||||
int $bluePoints
|
||||
): array {
|
||||
/** Initialize or load existing bonus stats */
|
||||
$redBonusStats = $playedGame->getRedBonusStats() ?? [
|
||||
'blindHits' => 0,
|
||||
'chainBest' => 0,
|
||||
'chainCurrent' => 0,
|
||||
'lastMineHits' => 0,
|
||||
'edgeMines' => 0,
|
||||
'biggestReveal' => 0,
|
||||
];
|
||||
$blueBonusStats = $playedGame->getBlueBonusStats() ?? [
|
||||
'blindHits' => 0,
|
||||
'chainBest' => 0,
|
||||
'chainCurrent' => 0,
|
||||
'lastMineHits' => 0,
|
||||
'edgeMines' => 0,
|
||||
'biggestReveal' => 0,
|
||||
];
|
||||
|
||||
$redBonusPoints = $playedGame->getRedBonusPoints() ?? 0;
|
||||
$blueBonusPoints = $playedGame->getBlueBonusPoints() ?? 0;
|
||||
|
||||
$isRed = 'red' === $player;
|
||||
$currentStats = $isRed ? $redBonusStats : $blueBonusStats;
|
||||
$bonusPoints = 0;
|
||||
|
||||
/** Track biggest reveal (safe cells count) if any safe cells were revealed */
|
||||
if ($safeCellsFound > 0) {
|
||||
if ($safeCellsFound > $currentStats['biggestReveal']) {
|
||||
$currentStats['biggestReveal'] = $safeCellsFound;
|
||||
}
|
||||
}
|
||||
|
||||
/** Only calculate bonuses if mines were found */
|
||||
if ($minesFound > 0) {
|
||||
/** Check Blind Hit: the clicked mine cell has no revealed numbered neighbors */
|
||||
if ($this->isBlindHit($coords, $grid, $alreadyRevealed)) {
|
||||
$currentStats['blindHits']++;
|
||||
$bonusPoints += 2;
|
||||
}
|
||||
|
||||
/** Check Edge Mine: the clicked cell is on the boundary */
|
||||
if ($this->isEdgeMine($coords)) {
|
||||
$currentStats['edgeMines']++;
|
||||
$bonusPoints += 1;
|
||||
}
|
||||
|
||||
/** Check Endgame Mine: when few mines remain on the board */
|
||||
$totalMinesOnBoard = 51;
|
||||
$minesRevealed = $redPoints + $bluePoints;
|
||||
$minesRemaining = $totalMinesOnBoard - $minesRevealed;
|
||||
|
||||
if ($minesRemaining <= 10) {
|
||||
$currentStats['lastMineHits']++;
|
||||
$bonusPoints += 3;
|
||||
}
|
||||
|
||||
/** Chain combo: increment consecutive mine-click counter */
|
||||
$currentStats['chainCurrent']++;
|
||||
if ($currentStats['chainCurrent'] > $currentStats['chainBest']) {
|
||||
$currentStats['chainBest'] = $currentStats['chainCurrent'];
|
||||
}
|
||||
} else {
|
||||
/** No mines found - reset chain for this player */
|
||||
$currentStats['chainCurrent'] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add points for safe cells revealed (each safe cell revealed = +0.5 bonus point)
|
||||
* Only award points if at least 2 safe cells were revealed
|
||||
*/
|
||||
if ($safeCellsFound >= 2) {
|
||||
$bonusPoints += ($safeCellsFound * 0.5);
|
||||
}
|
||||
|
||||
/** Update the appropriate player's stats and points */
|
||||
if ($isRed) {
|
||||
$redBonusStats = $currentStats;
|
||||
$redBonusPoints += $bonusPoints;
|
||||
} else {
|
||||
$blueBonusStats = $currentStats;
|
||||
$blueBonusPoints += $bonusPoints;
|
||||
}
|
||||
|
||||
/** Persist updated stats to the database */
|
||||
$playedGame->setRedBonusStats($redBonusStats);
|
||||
$playedGame->setBlueBonusStats($blueBonusStats);
|
||||
$playedGame->setRedBonusPoints($redBonusPoints);
|
||||
$playedGame->setBlueBonusPoints($blueBonusPoints);
|
||||
$this->em->persist($playedGame);
|
||||
|
||||
return [
|
||||
'redBonusPoints' => $redBonusPoints,
|
||||
'blueBonusPoints' => $blueBonusPoints,
|
||||
'redBonusStats' => $redBonusStats,
|
||||
'blueBonusStats' => $blueBonusStats,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mine was clicked with no revealed numbered neighbors (blind hit).
|
||||
* Returns true if none of the 8 surrounding cells show a number.
|
||||
*/
|
||||
private function isBlindHit(array $coords, array $grid, array $alreadyRevealed): bool
|
||||
{
|
||||
$row = $coords[0];
|
||||
$col = $coords[1];
|
||||
$dirs = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
|
||||
|
||||
foreach ($dirs as [$dr, $dc]) {
|
||||
$nr = $row + $dr;
|
||||
$nc = $col + $dc;
|
||||
$key = $nr . ',' . $nc;
|
||||
|
||||
/** Check if neighbor is revealed and is a numbered cell (not a mine, not hidden) */
|
||||
if (isset($alreadyRevealed[$key])) {
|
||||
$val = $grid[$nr][$nc] ?? null;
|
||||
|
||||
/** If it's a number (0-8), not a mine, it's revealed and visible */
|
||||
if (is_numeric($val) && $val >= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mine is on the edge/corner of the board.
|
||||
*/
|
||||
private function isEdgeMine(array $coords): bool
|
||||
{
|
||||
$row = $coords[0];
|
||||
$col = $coords[1];
|
||||
|
||||
return 0 === $row || $row === 15 || 0 === $col || $col === 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* BFS flood-fill starting at (row, col).
|
||||
* Reveals the clicked cell plus all connected zero-value cells and their non-mine borders.
|
||||
@@ -403,10 +565,6 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
return $mines;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Database helpers
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
private function getPlayedGame(string $gameAssoc): ?PlayedGame
|
||||
{
|
||||
return $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
|
||||
@@ -424,8 +582,8 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
{
|
||||
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||
$playedGame->setResign($color);
|
||||
$this->entityManager->persist($playedGame);
|
||||
$this->entityManager->flush();
|
||||
$this->em->persist($playedGame);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function saveStepToDb(
|
||||
@@ -435,6 +593,7 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
array $revealedCells,
|
||||
int $redPoints,
|
||||
int $bluePoints,
|
||||
array $bonusData = []
|
||||
): void {
|
||||
try {
|
||||
$playedGame = $this->getPlayedGame($gameAssoc);
|
||||
@@ -447,31 +606,44 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
$step->setRevealedCells($revealedCells);
|
||||
$step->setPlayedGame($playedGame);
|
||||
$step->setCreated(new DateTime());
|
||||
$this->entityManager->persist($step);
|
||||
$this->em->persist($step);
|
||||
|
||||
$playedGame->setRedPoints($redPoints);
|
||||
$playedGame->setBluePoints($bluePoints);
|
||||
$playedGame->setRedExplodedBomb((bool)$event['bomb'] && 'red' === $player ? true : null);
|
||||
$playedGame->setBlueExplodedBomb((bool)$event['bomb'] && 'blue' === $player ? true : null);
|
||||
$playedGame->setUpdated(new DateTime());
|
||||
$this->entityManager->persist($playedGame);
|
||||
|
||||
$this->entityManager->flush();
|
||||
/** Bonus data is already persisted in calculateBonuses, but we ensure it's up to date */
|
||||
if (!empty($bonusData)) {
|
||||
$playedGame->setRedBonusPoints($bonusData['redBonusPoints']);
|
||||
$playedGame->setBlueBonusPoints($bonusData['blueBonusPoints']);
|
||||
$playedGame->setRedBonusStats($bonusData['redBonusStats']);
|
||||
$playedGame->setBlueBonusStats($bonusData['blueBonusStats']);
|
||||
}
|
||||
|
||||
$this->em->persist($playedGame);
|
||||
$this->em->flush();
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
null !== $user
|
||||
? $this->saveRegisteredUser($userName, $count, $playedGame)
|
||||
: $this->saveAnonUser($userName, $count, $playedGame);
|
||||
: $this->saveAnonUser($userName, $count, $playedGame, $request);
|
||||
|
||||
$this->entityManager->persist($playedGame);
|
||||
$this->entityManager->flush();
|
||||
$this->em->persist($playedGame);
|
||||
$this->em->flush();
|
||||
|
||||
return $this->getUserCollection($playedGame);
|
||||
}
|
||||
@@ -495,13 +667,16 @@ 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 {
|
||||
$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());
|
||||
$this->entityManager->persist($anon);
|
||||
$this->em->persist($anon);
|
||||
|
||||
if ($count === 1) {
|
||||
$random = random_int(0, 1);
|
||||
@@ -518,8 +693,8 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
|
||||
private function getUserCollection(PlayedGame $playedGame): array
|
||||
{
|
||||
$redUser = $playedGame->getRed();
|
||||
$blueUser = $playedGame->getBlue();
|
||||
$redUser = $playedGame->getRed();
|
||||
$blueUser = $playedGame->getBlue();
|
||||
|
||||
return [
|
||||
'red' => null !== $redUser ? $redUser->getUsername() : '',
|
||||
@@ -527,11 +702,11 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
|
||||
'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : '',
|
||||
'redAvatar' => null !== $redUser && null !== $redUser->getAvatarPath()
|
||||
? $this->cacheManager->generateUrl($redUser->getAvatarPath(), 'avatar_thumb')
|
||||
: null,
|
||||
? $this->cacheManager->generateUrl($redUser->getAvatarPath(), 'avatar_thumb')
|
||||
: null,
|
||||
'blueAvatar' => null !== $blueUser && null !== $blueUser->getAvatarPath()
|
||||
? $this->cacheManager->generateUrl($blueUser->getAvatarPath(), 'avatar_thumb')
|
||||
: null,
|
||||
? $this->cacheManager->generateUrl($blueUser->getAvatarPath(), 'avatar_thumb')
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -585,4 +760,27 @@ readonly class TopicManager implements TopicManagerInterface
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="bshare-vs">
|
||||
<div class="bshare-player bshare-player--red">
|
||||
<div class="bshare-avatar bshare-avatar--red">
|
||||
<div class="bshare-avatar bshare-avatar--red" style="position: relative;">
|
||||
{% if redAvatar %}
|
||||
<img src="{{ redAvatar|imagine_filter('avatar_thumb') }}"
|
||||
alt="{{ redName }}"
|
||||
@@ -39,6 +39,11 @@
|
||||
{% else %}
|
||||
{{ redName|slice(0,2)|upper }}
|
||||
{% endif %}
|
||||
{% if redBonusPoints > blueBonusPoints %}
|
||||
<div style="position: absolute; bottom: -6px; right: -6px; background: #ffd700; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1); border: 2px solid rgba(0,0,0,0.5); z-index: 10;">
|
||||
<i class="fas fa-star" style="color: #000; font-size: 14px;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="bshare-player__name">{{ redName }}</span>
|
||||
<span class="bshare-player__side">Red</span>
|
||||
@@ -53,6 +58,15 @@
|
||||
{% else %}
|
||||
<div class="bshare-score bshare-score--na">— : —</div>
|
||||
{% endif %}
|
||||
<div style="display: flex; justify-content: center; gap: 0; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font: 700 13px 'Rajdhani', sans-serif; color: #f67d52; display: flex; align-items: center; gap: 4px;">
|
||||
<i class="fas fa-star" style="font-size: 11px;"></i> {{ (redBonusPoints ?? 0)|number_format(1, '.', '') }}
|
||||
</span>
|
||||
<span style="font: 700 13px 'Rajdhani', sans-serif; color: rgba(255,255,255,0.3); margin: 0 8px;">:</span>
|
||||
<span style="font: 700 13px 'Rajdhani', sans-serif; color: #95cff5; display: flex; align-items: center; gap: 4px;">
|
||||
{{ (blueBonusPoints ?? 0)|number_format(1, '.', '') }} <i class="fas fa-star" style="font-size: 11px;"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="bshare-vs__label">VS</div>
|
||||
{% if resign == 'red' %}
|
||||
<div class="bshare-badge bshare-badge--blue">
|
||||
@@ -79,7 +93,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="bshare-player bshare-player--blue">
|
||||
<div class="bshare-avatar bshare-avatar--blue">
|
||||
<div class="bshare-avatar bshare-avatar--blue" style="position: relative;">
|
||||
{% if blueAvatar %}
|
||||
<img src="{{ blueAvatar|imagine_filter('avatar_thumb') }}"
|
||||
alt="{{ blueName }}"
|
||||
@@ -87,6 +101,11 @@
|
||||
{% else %}
|
||||
{{ blueName|slice(0,2)|upper }}
|
||||
{% endif %}
|
||||
{% if blueBonusPoints > redBonusPoints %}
|
||||
<div style="position: absolute; bottom: -6px; right: -6px; background: #ffd700; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1); border: 2px solid rgba(0,0,0,0.5); z-index: 10;">
|
||||
<i class="fas fa-star" style="color: #000; font-size: 14px;"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="bshare-player__name">{{ blueName }}</span>
|
||||
<span class="bshare-player__side">Blue</span>
|
||||
@@ -118,6 +137,108 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Bonus Stats Section #}
|
||||
{% set hasRedStats = redBonusStats is not empty and (redBonusStats.blindHits or redBonusStats.chainBest or redBonusStats.edgeMines or redBonusStats.lastMineHits or redBonusStats.biggestReveal) %}
|
||||
{% set hasBlueStats = blueBonusStats is not empty and (blueBonusStats.blindHits or blueBonusStats.chainBest or blueBonusStats.edgeMines or blueBonusStats.lastMineHits or blueBonusStats.biggestReveal) %}
|
||||
{% if redBonusPoints > 0 or blueBonusPoints > 0 or hasRedStats or hasBlueStats %}
|
||||
<div class="bshare-bonus">
|
||||
<div class="bshare-bonus__title">
|
||||
<i class="fas fa-star"></i> Bonus Statistics
|
||||
</div>
|
||||
<div class="bshare-bonus__grid">
|
||||
{# Red Bonus #}
|
||||
<div class="bshare-bonus__player bshare-bonus__player--red">
|
||||
<div class="bshare-bonus__header">
|
||||
<span class="bshare-bonus__points">{{ redBonusPoints|number_format(1, '.', '') }}</span>
|
||||
<span class="bshare-bonus__label">pts</span>
|
||||
</div>
|
||||
<div class="bshare-bonus__stats">
|
||||
{% if redBonusStats is not empty and redBonusStats.blindHits %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Blind hits</span>
|
||||
<span class="bshare-bonus__stat-value">{{ redBonusStats.blindHits }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if redBonusStats is not empty and redBonusStats.chainBest %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Best chain</span>
|
||||
<span class="bshare-bonus__stat-value">{{ redBonusStats.chainBest }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if redBonusStats is not empty and redBonusStats.edgeMines %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Edge mines</span>
|
||||
<span class="bshare-bonus__stat-value">{{ redBonusStats.edgeMines }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if redBonusStats is not empty and redBonusStats.lastMineHits %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Endgame mines</span>
|
||||
<span class="bshare-bonus__stat-value">{{ redBonusStats.lastMineHits }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if redBonusStats is not empty and redBonusStats.biggestReveal %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Biggest reveal</span>
|
||||
<span class="bshare-bonus__stat-value">{{ redBonusStats.biggestReveal }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not hasRedStats %}
|
||||
<div class="bshare-bonus__stat bshare-bonus__stat--empty">
|
||||
<span class="bshare-bonus__stat-label">No bonuses earned</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Blue Bonus #}
|
||||
<div class="bshare-bonus__player bshare-bonus__player--blue">
|
||||
<div class="bshare-bonus__header">
|
||||
<span class="bshare-bonus__points">{{ blueBonusPoints|number_format(1, '.', '') }}</span>
|
||||
<span class="bshare-bonus__label">pts</span>
|
||||
</div>
|
||||
<div class="bshare-bonus__stats">
|
||||
{% if blueBonusStats is not empty and blueBonusStats.blindHits %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Blind hits</span>
|
||||
<span class="bshare-bonus__stat-value">{{ blueBonusStats.blindHits }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if blueBonusStats is not empty and blueBonusStats.chainBest %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Best chain</span>
|
||||
<span class="bshare-bonus__stat-value">{{ blueBonusStats.chainBest }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if blueBonusStats is not empty and blueBonusStats.edgeMines %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Edge mines</span>
|
||||
<span class="bshare-bonus__stat-value">{{ blueBonusStats.edgeMines }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if blueBonusStats is not empty and blueBonusStats.lastMineHits %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Endgame mines</span>
|
||||
<span class="bshare-bonus__stat-value">{{ blueBonusStats.lastMineHits }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if blueBonusStats is not empty and blueBonusStats.biggestReveal %}
|
||||
<div class="bshare-bonus__stat">
|
||||
<span class="bshare-bonus__stat-label">Biggest reveal</span>
|
||||
<span class="bshare-bonus__stat-value">{{ blueBonusStats.biggestReveal }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not hasBlueStats %}
|
||||
<div class="bshare-bonus__stat bshare-bonus__stat--empty">
|
||||
<span class="bshare-bonus__stat-label">No bonuses earned</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bshare-cta">
|
||||
<a href="{{ path('MineSeekerBundle_gamePlay') }}" class="bshare-btn">
|
||||
<i class="fas fa-play"></i> Play MineSeeker
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - The Game{% endblock %}
|
||||
|
||||
{% 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:type" content="website"/>
|
||||
<meta property="og:site_name" content="MineSeeker"/>
|
||||
@@ -23,9 +23,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<section
|
||||
class="hero{% if app.request.attributes.get('_route') != 'MineSeekerBundle_homepage' %} hero--compact{% endif %}">
|
||||
|
||||
<section id="hero-auth">
|
||||
<div class="hero-auth">
|
||||
{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
|
||||
<a href="{{ path('MineSeekerBundle_profile') }}" class="hero-auth-btn hero-auth-btn--profile">
|
||||
@@ -56,7 +54,10 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="hero{% if app.request.attributes.get('_route') != 'MineSeekerBundle_homepage' %} hero--compact{% endif %}">
|
||||
<a class="hero-logo" href="{{ path('MineSeekerBundle_homepage') }}">
|
||||
<img src="{{ asset('images/mine-logo-txt.png') }}" alt="MineSeeker"/>
|
||||
</a>
|
||||
@@ -253,6 +254,7 @@
|
||||
<p class="footer-nav-label">Navigate</p>
|
||||
<ul>
|
||||
<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_privacy') }}">Privacy Policy</a></li>
|
||||
<li><a href="{{ path('MineSeekerBundle_contact') }}">Contact</a></li>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<meta property="og:title" content="Your friend challenges YOU!"/>
|
||||
<meta property="og:description" content="Do you accept the challenge?"/>
|
||||
<meta property="og:image"
|
||||
content="{{ app.request.getSchemeAndHttpHost() }}{{ asset('/images/mine-1600x627.png') }}"/>
|
||||
content="https://{{ app.request.host }}{{ asset('/images/mine-1600x627.png') }}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Contact{% endblock %}
|
||||
|
||||
{% 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:type" content="website"/>
|
||||
<meta property="og:site_name" content="MineSeeker"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Privacy Policy{% endblock %}
|
||||
|
||||
{% 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:type" content="website"/>
|
||||
<meta property="og:site_name" content="MineSeeker"/>
|
||||
|
||||
144
templates/Official/rules.html.twig
Normal file
144
templates/Official/rules.html.twig
Normal 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 — <strong>Red</strong> and <strong>Blue</strong> — 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×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 — 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>“It is your turn! Make a move”</em>; while you wait it reads <em>“Your buddy is making a move”</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 — no points are awarded — 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 & 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’s flag and scores <strong>one point</strong> for you.</li>
|
||||
<li>You keep the turn and may click again — rack up a streak while you’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×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> — the first player to claim more than half of the mines (26 of 51) wins immediately.</li>
|
||||
<li><strong>A player resigns</strong> — the remaining player wins.</li>
|
||||
<li><strong>Draw</strong> — 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’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 & 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 — 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’ 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 %}
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Terms of Service{% endblock %}
|
||||
|
||||
{% 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:type" content="website"/>
|
||||
<meta property="og:site_name" content="MineSeeker"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Forgot Password{% endblock %}
|
||||
|
||||
{% 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 property="og:url" content="{{ app.request.uri }}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Sign In{% endblock %}
|
||||
|
||||
{% 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 property="og:url" content="{{ app.request.uri }}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Profile{% endblock %}
|
||||
|
||||
{% 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 property="og:url" content="{{ url('MineSeekerBundle_profile') | replace({'http://': 'https://'}) }}"/>
|
||||
<meta property="og:type" content="profile"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Security Settings{% endblock %}
|
||||
|
||||
{% 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 property="og:url" content="{{ url('MineSeekerBundle_profile_security') | replace({'http://': 'https://'}) }}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %} - Register{% endblock %}
|
||||
|
||||
{% 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:type" content="website"/>
|
||||
<meta property="og:site_name" content="MineSeeker"/>
|
||||
@@ -117,6 +117,25 @@
|
||||
</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">
|
||||
<i class="fas fa-user-plus"></i> Create Account
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user