4
0

Compare commits

..

87 Commits

Author SHA1 Message Date
a523951207 branch into version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 20:17:02 +01:00
429857e7cb inc fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 19:29:52 +01:00
07fe9ee150 header tweak
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-28 00:10:37 +01:00
d5becf0fdc add name to header
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-27 22:02:17 +01:00
e4ed56a9d3 meta.header.lua
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-27 15:25:57 +01:00
e4241fc2d2 pipeline update
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-27 15:21:04 +01:00
72ac79bd81 tic80 image change
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-13 22:52:12 +01:00
ec3f7a91ae pipeline local registry
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 00:47:32 +01:00
c150711514 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 10:24:42 +01:00
24c56ad23d pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 10:21:38 +01:00
601a9aa1ad pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 10:12:21 +01:00
b48e3b88a4 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 10:03:42 +01:00
94deee7154 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 10:01:58 +01:00
ddabdaaa29 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 09:54:39 +01:00
30e160a94b pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 09:53:14 +01:00
89519cb7a3 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 09:28:27 +01:00
408c56a421 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 09:25:54 +01:00
c43156e95f pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 09:24:20 +01:00
2a7d6fccfc pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 09:05:31 +01:00
f564bb4616 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-07 01:21:36 +01:00
a855c37128 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-07 01:19:03 +01:00
a3b614400b html content
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-07 01:08:15 +01:00
570e57371f pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 23:45:15 +01:00
e8c3e41988 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 23:06:13 +01:00
1e1fa3f538 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 22:52:26 +01:00
182623a846 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 22:49:50 +01:00
727d011601 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 21:57:20 +01:00
c89f5bd7dc readme fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 17:43:25 +01:00
5e78f275c1 remove serve
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 16:49:03 +01:00
698b274d05 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 16:02:23 +01:00
98ad8458fa remove prebuild game 2025-12-06 15:58:56 +01:00
efc6b020e6 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 15:57:59 +01:00
e34375f05e pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 15:56:04 +01:00
ae7a8921a0 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 15:52:41 +01:00
161390fb65 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 14:51:26 +01:00
256b594076 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 14:50:46 +01:00
9844557a0a pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 14:49:47 +01:00
420dd71423 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 14:34:49 +01:00
985193b89f pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 14:32:40 +01:00
0459b2e6e5 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 13:25:34 +01:00
12d00c80a3 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 13:23:02 +01:00
9a561552b5 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:19:01 +01:00
eacbdadf67 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:17:02 +01:00
4189d2835f pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:14:26 +01:00
7d5928940c pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:09:20 +01:00
f32f33faa3 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:08:33 +01:00
fa77eb92cc pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:08:02 +01:00
0183b02491 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:07:00 +01:00
0c8fb9cfa1 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:06:15 +01:00
df23351665 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:05:22 +01:00
97e9494fb2 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:04:44 +01:00
e58938c9d5 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 13:02:24 +01:00
c8c900dbd8 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:58:35 +01:00
64e15c3f18 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:54:25 +01:00
e417be4c1f pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:53:52 +01:00
888b1da1bd pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 12:51:39 +01:00
21520e9922 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 12:47:50 +01:00
ab44ddd1b4 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:46:34 +01:00
3877165783 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:45:09 +01:00
055e1855e0 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:41:37 +01:00
9bf60b6f6b pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:35:04 +01:00
c79f9ee4d5 pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 12:34:13 +01:00
93ae09bd94 pipeline fix 2025-12-06 12:32:03 +01:00
aed67091a3 pipeline fix 2025-12-06 12:30:28 +01:00
c3b26f9dcf pipeline fix 2025-12-06 10:40:10 +01:00
978055c02a pipeline fix 2025-12-06 10:34:35 +01:00
4048bd39e8 pipeline fix 2025-12-06 10:34:06 +01:00
328350b6c6 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 10:30:31 +01:00
15af10d313 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 10:30:05 +01:00
00755a383f pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 10:28:19 +01:00
95eccf0fbc pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 10:27:12 +01:00
a26d1ddfd4 pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 10:25:36 +01:00
f05e89785a pipeline fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-06 10:24:00 +01:00
63292900ac pipeline fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-06 10:18:13 +01:00
8a41066410 pipeline fix 2025-12-06 10:17:32 +01:00
e21d35a47d pipeline fix 2025-12-06 10:16:39 +01:00
86b109f18a add woodpecker 2025-12-06 10:14:20 +01:00
b579692d42 feat: Add controller B button for back action 2025-12-05 01:32:45 +01:00
e9e0ef9da2 fix: Remove "Clone" from main menu 2025-12-05 01:30:37 +01:00
e9c53a5717 fix: Remove "Clone" from boot splash screen 2025-12-05 01:29:48 +01:00
05564c3136 feat: Rename Bomberman to BombExpert 2025-12-05 01:29:08 +01:00
Zsolt Tasnadi
04405066c4 refact 2025-12-04 17:52:35 +01:00
Zsolt Tasnadi
94a412d168 refact 2025-12-04 17:46:24 +01:00
Zsolt Tasnadi
70f18d4f3b settings menu 2025-12-04 17:39:27 +01:00
Zsolt Tasnadi
bc1944b163 refact 2025-12-04 17:32:20 +01:00
Zsolt Tasnadi
f47bd6b2e0 refact 2025-12-04 17:27:07 +01:00
Zsolt Tasnadi
b56eb8cdb4 README update 2025-12-04 17:00:21 +01:00
8 changed files with 775 additions and 283 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.local .local
.vscode .vscode
.DS_Store .DS_Store
bomberman.zip bombexpert.lua
bomberman/* bombexpert/*

36
.woodpecker.yml Normal file
View File

@@ -0,0 +1,36 @@
steps:
- name: version
image: alpine
commands:
- 'apk add --no-cache make'
- 'make ci-version'
- name: build
image: git.teletype.hu/internal/tic80pro:latest
environment:
XDG_RUNTIME_DIR: /tmp
commands:
- 'make ci-export'
- name: artifact
image: alpine
environment:
DROPAREA_HOST: vps.teletype.hu
DROPAREA_PORT: 2223
DROPAREA_TARGET_PATH: /home/drop
DROPAREA_USER: drop
DROPAREA_SSH_PASSWORD:
from_secret: droparea_ssh_password
commands:
- 'apk add --no-cache make openssh-client sshpass'
- 'make ci-upload'
- name: update
image: alpine
environment:
UPDATE_SERVER: https://games.vps.teletype.hu
UPDATE_SECRET:
from_secret: update_secret_key
commands:
- 'apk add --no-cache make curl'
- 'make ci-update'

115
Makefile Normal file
View File

@@ -0,0 +1,115 @@
# -----------------------------------------
# Makefile TIC-80 project builder
# -----------------------------------------
PROJECT = bombexpert
ORDER = $(PROJECT).inc
OUTPUT = $(PROJECT).lua
OUTPUT_ZIP = $(PROJECT).html.zip
OUTPUT_TIC = $(PROJECT).tic
SRC_DIR = inc
SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
ASSETS_LUA = inc/meta/meta.assets.lua
# CI/CD variables
VERSION_FILE = .version
GAME_LANG ?= lua
DROPAREA_HOST ?= vps.teletype.hu
DROPAREA_PORT ?= 2223
DROPAREA_TARGET_PATH ?= /home/drop
DROPAREA_USER ?= drop
UPDATE_SERVER ?= https://games.vps.teletype.hu
all: build
build: $(OUTPUT)
$(OUTPUT): $(SRC) $(ORDER)
@rm -f $(OUTPUT)
@while read f; do \
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
echo "" >> $(OUTPUT); \
done < $(ORDER)
export: build
@if [ -z "$(VERSION)" ]; then \
echo "ERROR: VERSION not set!"; \
exit 1; \
fi
@echo "==> Exporting HTML for version $(VERSION)"
@tic80 --cli --skip --fs=. \
--cmd="load $(OUTPUT) & save $(PROJECT)-$(VERSION) & export html $(PROJECT)-$(VERSION).html & exit"
@echo "==> Creating versioned files"
@if [ -f "$(PROJECT)-$(VERSION).tic" ]; then \
cp $(PROJECT)-$(VERSION).tic $(PROJECT).tic; \
fi
@if [ -f "$(PROJECT)-$(VERSION).html.zip" ]; then \
cp $(PROJECT)-$(VERSION).html.zip $(PROJECT).html.zip; \
fi
@echo "==> Generated files:"
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
watch:
make build
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
import_assets:
@for t in $(ASSET_TYPES); do \
for f in $(ASSETS_DIR)/$$t/*.png; do \
[ -e "$$f" ] || continue; \
echo "==> Importing $$f as $$t..."; \
tic80 --cli --skip --fs=. --cmd="import $$t $$f & exit"; \
done; \
done
export_assets: build
@echo "==> Exporting TIC-80 asset sections"
@mkdir -p inc/meta
@sed -n '/^-- <PALETTE>/,/^-- <\/PALETTE>/p;\
/^-- <TILES>/,/^-- <\/TILES>/p;\
/^-- <SPRITES>/,/^-- <\/SPRITES>/p;\
/^-- <MAP>/,/^-- <\/MAP>/p;\
/^-- <SFX>/,/^-- <\/SFX>/p;\
/^-- <MUSIC>/,/^-- <\/MUSIC>/p' \
$(OUTPUT) > $(ASSETS_LUA)
clean:
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(OUTPUT)
@echo "==> Cleaned build artifacts"
# CI/CD Targets
ci-version:
@VERSION=$$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]"); \
BRANCH=$${CI_COMMIT_BRANCH:-$${WOODPECKER_BRANCH}}; \
if [ "$$BRANCH" != "main" ] && [ "$$BRANCH" != "master" ] && [ -n "$$BRANCH" ]; then \
VERSION=dev-$$VERSION-$$BRANCH; \
fi; \
echo "VERSION is: $$VERSION"; \
echo $$VERSION > $(VERSION_FILE)
ci-export:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Building and exporting version $$VERSION"; \
$(MAKE) export VERSION=$$VERSION
ci-upload:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Uploading artifacts for version $$VERSION"; \
ls -lh $(PROJECT)-$$VERSION.* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true; \
cp $(PROJECT).lua $(PROJECT)-$$VERSION.lua; \
FILE_LUA=$(PROJECT)-$$VERSION.lua; \
FILE_TIC=$(PROJECT)-$$VERSION.tic; \
FILE_HTML_ZIP=$(PROJECT)-$$VERSION.html.zip; \
SCP_TARGET="$(DROPAREA_USER)@$(DROPAREA_HOST):$(DROPAREA_TARGET_PATH)/"; \
sshpass -p "$(DROPAREA_SSH_PASSWORD)" scp -o StrictHostKeyChecking=no -P $(DROPAREA_PORT) $$FILE_LUA $$FILE_TIC $$FILE_HTML_ZIP $$SCP_TARGET
ci-update:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Triggering update for version $$VERSION"; \
curl "$(UPDATE_SERVER)/update?secret=$(UPDATE_SECRET)&name=$(PROJECT)&platform=tic80&version=$$VERSION"
.PHONY: all build export watch import_assets export_assets clean ci-version ci-export ci-upload ci-update

139
README.md
View File

@@ -1,93 +1,80 @@
# Bomberman
# BombExpert
A classic Bomberman clone for [TIC-80](https://tic80.com/) fantasy console.
## Getting started ## Features
To make it easy for you to get started with GitLab, here's a list of recommended next steps. - 1 Player mode (vs AI)
- 2 Player local multiplayer
- Grid-based movement with smooth animation
- Destructible walls
- Power-ups:
- **B** (yellow): +1 bomb capacity
- **P** (orange): +1 blast range
- Smart AI opponent that seeks power-ups and avoids explosions
- Score tracking across rounds
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! ## Controls
## Add your files ### Player 1 (Blue)
| Action | Key |
|--------|-----|
| Move | Arrow Keys |
| Place Bomb | Space |
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files ### Player 2 (Red)
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: | Action | Key |
|--------|-----|
| Move | W, A, S, D |
| Place Bomb | G |
``` ### Menu Navigation
cd existing_repo | Action | Key |
git remote add origin https://tea.zenheads.hu/zsolt.tasnadi/bomberman.git |--------|-----|
git branch -M main | Navigate | Up / Down |
git push -uf origin main | Select | Space |
| Back / Exit | Backspace |
## How to Play
1. Run the game in TIC-80
2. Select "1 Player Game" or "2 Player Game" from the menu
3. Navigate through the maze and place bombs to destroy breakable walls
4. Collect power-ups to increase your bomb capacity and blast range
5. Eliminate your opponent by catching them in an explosion
6. First player to win the round scores a point
## Running the Game
### In TIC-80
```bash
load bombexpert.lua
run
``` ```
## Integrate with your tools ### In Browser
Use the HTML export in the `bombexpert/` folder with the included server:
```bash
python serve.py
```
Then open http://localhost:3333 in your browser.
- [ ] [Set up project integrations](https://tea.zenheads.hu/zsolt.tasnadi/bomberman/-/settings/integrations) ## Requirements
## Collaborate with your team - [TIC-80](https://tic80.com/) fantasy console (free version works)
- Or any modern web browser (for HTML export)
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) ## Credits
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy - **Author**: Zsolt Tasnadi
- **Powered by**: Claude
Use the built-in continuous integration in GitLab. - **Sponsored by**: Zen Heads
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License ## License
For open source projects, say how it is licensed.
## Project status MIT License
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
---
Happy X-MAS!

2
bombexpert.inc Normal file
View File

@@ -0,0 +1,2 @@
meta/meta.header.lua
system/system.allin.lua

11
inc/meta/meta.header.lua Normal file
View File

@@ -0,0 +1,11 @@
-- title: BombExpert
-- name: bombexpert
-- author: Zsolt Tasnadi
-- desc: Simple BombExpert for TIC-80
-- site: http://teletype.hu
-- license: MIT License
-- version: 0.2
-- script: lua
-- luacheck: globals TIC btn btnp cls rect spr print exit sfx keyp key
-- luacheck: max line length 150

View File

@@ -1,14 +1,3 @@
-- title: Bomberman Clone
-- author: Zsolt Tasnadi
-- desc: Simple Bomberman clone for TIC-80
-- site: http://teletype.hu
-- license: MIT License
-- version: 0.2
-- script: lua
-- luacheck: globals TIC btn btnp cls rect spr print exit sfx keyp key
-- luacheck: max line length 150
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Constants -- Constants
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -25,18 +14,6 @@ local EMPTY = 0
local SOLID_WALL = 1 local SOLID_WALL = 1
local BREAKABLE_WALL = 2 local BREAKABLE_WALL = 2
-- Timing constants
local BOMB_TIMER = 90
local EXPLOSION_TIMER = 30
local SPREAD_DELAY = 6 -- ticks per cell spread
local SPLASH_DURATION = 90 -- 1.5 seconds at 60fps
local WIN_SCREEN_DURATION = 60
local AI_MOVE_DELAY = 20
local AI_BOMB_COOLDOWN = 90
-- Movement
local MOVE_SPEED = 2
-- Directions (up, down, left, right) -- Directions (up, down, left, right)
local DIRECTIONS = { local DIRECTIONS = {
{0, -1}, {0, -1},
@@ -44,6 +21,7 @@ local DIRECTIONS = {
{-1, 0}, {-1, 0},
{1, 0} {1, 0}
} }
local SPREAD_DIRS = {-1, 1} -- negative and positive spread directions
-- Sprite indices (SPRITES section loads at 256+) -- Sprite indices (SPRITES section loads at 256+)
local PLAYER_BLUE = 256 local PLAYER_BLUE = 256
@@ -71,9 +49,76 @@ local GAME_STATE_MENU = 1
local GAME_STATE_PLAYING = 2 local GAME_STATE_PLAYING = 2
local GAME_STATE_HELP = 3 local GAME_STATE_HELP = 3
local GAME_STATE_CREDITS = 4 local GAME_STATE_CREDITS = 4
local GAME_STATE_SETTINGS = 5
-- Powerup spawn chance --------------------------------------------------------------------------------
local POWERUP_SPAWN_CHANCE = 0.3 -- Game Configuration (easy to tweak game parameters)
--------------------------------------------------------------------------------
local Config = {
-- Player settings
player = {
move_speed = 2,
start_bombs = 1,
start_power = 1,
},
-- Bomb settings
bomb = {
timer = 90,
explosion_duration = 30,
spread_delay = 6,
},
-- AI settings
ai = {
move_delay = 20,
bomb_cooldown = 90,
danger_threshold = 30,
},
-- Map settings
map = {
breakable_wall_chance = 0.7,
powerup_spawn_chance = 0.3,
generator = "classic",
},
-- Timing
timing = {
splash_duration = 90,
win_screen_duration = 60,
},
}
--------------------------------------------------------------------------------
-- Sound System (centralized audio management)
--------------------------------------------------------------------------------
local Sound = {
effects = {
explosion = {
id = 0,
note = nil,
duration = 30
},
pickup = {
id = 1,
note = nil,
duration = 8
},
-- Add new sounds here:
-- menu_select = {id = 2, note = nil, duration = 10},
-- player_death = {id = 3, note = nil, duration = 20},
}
}
function Sound.play(effect_name)
local effect = Sound.effects[effect_name]
if effect then
sfx(effect.id, effect.note, effect.duration)
end
end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Modules -- Modules
@@ -88,6 +133,7 @@ local Splash = {}
local Menu = {} local Menu = {}
local Help = {} local Help = {}
local Credits = {} local Credits = {}
local Settings = {}
local WinScreen = {} local WinScreen = {}
local GameBoard = {} local GameBoard = {}
local Bomb = {} local Bomb = {}
@@ -101,8 +147,10 @@ local Game = {}
local State = { local State = {
game_state = GAME_STATE_SPLASH, game_state = GAME_STATE_SPLASH,
splash_timer = SPLASH_DURATION, splash_timer = 0, -- Will be set from Config on first frame
initialized = false, -- Config loaded flag
menu_selection = 1, menu_selection = 1,
settings_selection = 1,
two_player_mode = false, two_player_mode = false,
players = {}, players = {},
powerups = {}, powerups = {},
@@ -132,14 +180,18 @@ local POWERUP_TYPES = {
weight = 50, weight = 50,
color = COLOR_YELLOW, color = COLOR_YELLOW,
label = "B", label = "B",
apply = function(player) player.maxBombs = player.maxBombs + 1 end apply = function(player)
player.maxBombs = player.maxBombs + 1
end
}, },
{ {
type = "power", type = "power",
weight = 50, weight = 50,
color = COLOR_ORANGE, color = COLOR_ORANGE,
label = "P", label = "P",
apply = function(player) player.bombPower = player.bombPower + 1 end apply = function(player)
player.bombPower = player.bombPower + 1
end
}, },
} }
@@ -176,7 +228,7 @@ function Powerup.init()
State.powerups = {} State.powerups = {}
for row = 1, MAP_HEIGHT do for row = 1, MAP_HEIGHT do
for col = 1, MAP_WIDTH do for col = 1, MAP_WIDTH do
if State.map[row][col] == BREAKABLE_WALL and math.random() < POWERUP_SPAWN_CHANCE then if State.map[row][col] == BREAKABLE_WALL and math.random() < Config.map.powerup_spawn_chance then
table.insert(State.powerups, { table.insert(State.powerups, {
gridX = col, gridX = col,
gridY = row, gridY = row,
@@ -209,7 +261,7 @@ function Powerup.check_pickup()
local config = Powerup.get_config(pw.type) local config = Powerup.get_config(pw.type)
config.apply(player) config.apply(player)
table.remove(State.powerups, i) table.remove(State.powerups, i)
sfx(1, nil, 8) Sound.play("pickup")
end end
end end
end end
@@ -224,7 +276,7 @@ function Input.action_pressed()
end end
function Input.back_pressed() function Input.back_pressed()
return keyp(51) -- Backspace key return keyp(51) or btnp(5) -- Backspace key or B button
end end
function Input.up() function Input.up()
@@ -251,6 +303,14 @@ function Input.down_pressed()
return btnp(1) return btnp(1)
end end
function Input.left_pressed()
return btnp(2)
end
function Input.right_pressed()
return btnp(3)
end
-- Player 2 inputs (WASD + G for bomb) -- Player 2 inputs (WASD + G for bomb)
function Input.p2_up() function Input.p2_up()
return key(23) or btn(8) -- W key or gamepad 2 up return key(23) or btn(8) -- W key or gamepad 2 up
@@ -320,28 +380,95 @@ function Map.is_spawn_area(row, col)
return false return false
end end
function Map.generate() --------------------------------------------------------------------------------
-- Map Generators (extensible map generation system)
--------------------------------------------------------------------------------
local MapGenerators = {}
-- Classic Bomberman grid pattern
function MapGenerators.classic(row, col)
if row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
return SOLID_WALL
elseif math.random() < Config.map.breakable_wall_chance then
return BREAKABLE_WALL
end
return EMPTY
end
-- Open arena with fewer pillars
function MapGenerators.arena(row, col)
-- Only pillars at every 4th position
if row % 4 == 1 and col % 4 == 1 and row > 1 and col > 1 then
return SOLID_WALL
elseif math.random() < Config.map.breakable_wall_chance * 0.5 then
return BREAKABLE_WALL
end
return EMPTY
end
-- Dense maze with more walls
function MapGenerators.maze(row, col)
if row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
return SOLID_WALL
elseif math.random() < 0.85 then
return BREAKABLE_WALL
end
return EMPTY
end
-- Corridors pattern
function MapGenerators.corridors(row, col)
-- Horizontal corridors at rows 4, 8, 12
if (row == 4 or row == 8 or row == 12) and col > 1 and col < MAP_WIDTH then
if math.random() < 0.3 then
return BREAKABLE_WALL
end
return EMPTY
end
-- Vertical corridors at cols 7, 14, 21
if (col == 7 or col == 14 or col == 21) and row > 1 and row < MAP_HEIGHT then
if math.random() < 0.3 then
return BREAKABLE_WALL
end
return EMPTY
end
-- Rest is classic pattern
if row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
return SOLID_WALL
elseif math.random() < Config.map.breakable_wall_chance then
return BREAKABLE_WALL
end
return EMPTY
end
function Map.generate(generator_name)
generator_name = generator_name or Config.map.generator
local generator = MapGenerators[generator_name] or MapGenerators.classic
for row = 1, MAP_HEIGHT do for row = 1, MAP_HEIGHT do
for col = 1, MAP_WIDTH do for col = 1, MAP_WIDTH do
-- Border walls -- Border walls (always)
if row == 1 or row == MAP_HEIGHT or col == 1 or col == MAP_WIDTH then if row == 1 or row == MAP_HEIGHT or col == 1 or col == MAP_WIDTH then
State.map[row][col] = SOLID_WALL State.map[row][col] = SOLID_WALL
-- Spawn areas MUST be empty -- Spawn areas MUST be empty (always)
elseif Map.is_spawn_area(row, col) then elseif Map.is_spawn_area(row, col) then
State.map[row][col] = EMPTY State.map[row][col] = EMPTY
-- Grid pattern solid walls (odd row AND odd col, but not border) -- Use selected generator for the rest
elseif row % 2 == 1 and col % 2 == 1 and row > 1 and col > 1 then
State.map[row][col] = SOLID_WALL
-- Random: breakable wall or empty
else else
if math.random() < 0.7 then State.map[row][col] = generator(row, col)
State.map[row][col] = BREAKABLE_WALL
else
State.map[row][col] = EMPTY
end end
end end
end end
end end
-- Helper to get available generators
function Map.get_generators()
local names = {}
for name, _ in pairs(MapGenerators) do
table.insert(names, name)
end
return names
end end
function Map.draw_shadows() function Map.draw_shadows()
@@ -418,10 +545,17 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
function Splash.update() function Splash.update()
-- Initialize on first frame
if not State.initialized then
Settings.load()
State.splash_timer = Config.timing.splash_duration
State.initialized = true
end
cls(COLOR_BLACK) cls(COLOR_BLACK)
UI.print_shadow("Bomberman", 85, 50, COLOR_BLUE, false, 2) UI.print_shadow("BombExpert", 85, 50, COLOR_BLUE, false, 2)
UI.print_shadow("Clone", 100, 70, COLOR_BLUE, false, 2)
State.splash_timer = State.splash_timer - 1 State.splash_timer = State.splash_timer - 1
if State.splash_timer <= 0 then if State.splash_timer <= 0 then
@@ -433,52 +567,74 @@ end
-- Menu module -- Menu module
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
local MENU_ITEMS = {
{
label = "1 Player Game",
action = function()
State.two_player_mode = false
State.game_state = GAME_STATE_PLAYING
Game.init()
end
},
{
label = "2 Player Game",
action = function()
State.two_player_mode = true
State.game_state = GAME_STATE_PLAYING
Game.init()
end
},
{
label = "Settings",
action = function()
State.game_state = GAME_STATE_SETTINGS
end
},
{
label = "Help",
action = function()
State.game_state = GAME_STATE_HELP
end
},
{
label = "Credits",
action = function()
State.game_state = GAME_STATE_CREDITS
end
},
{
label = "Exit",
action = exit
},
}
local function get_menu_color(index)
return (State.menu_selection == index) and COLOR_CYAN or COLOR_GRAY_LIGHT
end
function Menu.update() function Menu.update()
cls(COLOR_BLACK) cls(COLOR_BLACK)
UI.print_shadow("Bomberman", 85, 20, COLOR_BLUE, false, 2) UI.print_shadow("BombExpert", 85, 20, COLOR_BLUE, false, 2)
UI.print_shadow("Clone", 100, 40, COLOR_BLUE, false, 2)
local unselected = COLOR_GRAY_LIGHT
local p1_color = (State.menu_selection == 1) and COLOR_CYAN or unselected
local p2_color = (State.menu_selection == 2) and COLOR_CYAN or unselected
local help_color = (State.menu_selection == 3) and COLOR_CYAN or unselected
local credits_color = (State.menu_selection == 4) and COLOR_CYAN or unselected
local exit_color = (State.menu_selection == 5) and COLOR_CYAN or unselected
local cursor_y = 60 + (State.menu_selection - 1) * 14 local cursor_y = 60 + (State.menu_selection - 1) * 14
UI.print_shadow(">", 60, cursor_y, COLOR_CYAN) UI.print_shadow(">", 60, cursor_y, COLOR_CYAN)
UI.print_shadow("1 Player Game", 70, 60, p1_color) for i, item in ipairs(MENU_ITEMS) do
UI.print_shadow("2 Player Game", 70, 74, p2_color) UI.print_shadow(item.label, 70, 60 + (i - 1) * 14, get_menu_color(i))
UI.print_shadow("Help", 70, 88, help_color) end
UI.print_shadow("Credits", 70, 102, credits_color)
UI.print_shadow("Exit", 70, 116, exit_color)
if Input.back_pressed() then if Input.back_pressed() then
exit() exit()
elseif Input.up_pressed() then elseif Input.up_pressed() then
State.menu_selection = State.menu_selection - 1 State.menu_selection = State.menu_selection - 1
if State.menu_selection < 1 then State.menu_selection = 5 end if State.menu_selection < 1 then State.menu_selection = #MENU_ITEMS end
elseif Input.down_pressed() then elseif Input.down_pressed() then
State.menu_selection = State.menu_selection + 1 State.menu_selection = State.menu_selection + 1
if State.menu_selection > 5 then State.menu_selection = 1 end if State.menu_selection > #MENU_ITEMS then State.menu_selection = 1 end
elseif Input.action_pressed() then elseif Input.action_pressed() then
if State.menu_selection == 1 then MENU_ITEMS[State.menu_selection].action()
State.two_player_mode = false
State.game_state = GAME_STATE_PLAYING
Game.init()
elseif State.menu_selection == 2 then
State.two_player_mode = true
State.game_state = GAME_STATE_PLAYING
Game.init()
elseif State.menu_selection == 3 then
State.game_state = GAME_STATE_HELP
elseif State.menu_selection == 4 then
State.game_state = GAME_STATE_CREDITS
else
exit()
end
end end
end end
@@ -552,6 +708,224 @@ function Credits.update()
end end
end end
--------------------------------------------------------------------------------
-- Settings module (persistent settings menu)
--------------------------------------------------------------------------------
-- luacheck: globals pmem
-- Settings definition: each setting maps to a pmem slot
-- pmem stores integers, so we use multipliers for decimals
local SETTINGS_ITEMS = {
{
label = "Start Bombs",
path = {"player", "start_bombs"},
min = 1,
max = 5,
step = 1,
pmem_slot = 0
},
{
label = "Start Power",
path = {"player", "start_power"},
min = 1,
max = 5,
step = 1,
pmem_slot = 1
},
{
label = "Move Speed",
path = {"player", "move_speed"},
min = 1,
max = 4,
step = 1,
pmem_slot = 2
},
{
label = "Bomb Timer",
path = {"bomb", "timer"},
min = 60,
max = 180,
step = 15,
pmem_slot = 3
},
{
label = "AI Speed",
path = {"ai", "move_delay"},
min = 10,
max = 40,
step = 5,
pmem_slot = 4
},
{
label = "Wall Density",
path = {"map", "breakable_wall_chance"},
min = 30,
max = 90,
step = 10,
pmem_slot = 5,
multiplier = 100
},
{
label = "Powerup Chance",
path = {"map", "powerup_spawn_chance"},
min = 10,
max = 50,
step = 5,
pmem_slot = 6,
multiplier = 100
},
{
label = "Map Style",
path = {"map", "generator"},
min = 1,
max = 4,
step = 1,
pmem_slot = 7,
is_enum = true,
enum_values = {"classic", "arena", "maze", "corridors"}
},
}
-- Magic number to detect if pmem has been initialized
local PMEM_INIT_SLOT = 255
local PMEM_INIT_VALUE = 12345
local function get_config_value(item)
local value = Config
for _, key in ipairs(item.path) do
value = value[key]
end
if item.multiplier then
return math.floor(value * item.multiplier + 0.5)
end
if item.is_enum then
for i, v in ipairs(item.enum_values) do
if v == value then return i end
end
return 1
end
return value
end
local function set_config_value(item, value)
local target = Config
for i = 1, #item.path - 1 do
target = target[item.path[i]]
end
local final_key = item.path[#item.path]
if item.multiplier then
target[final_key] = value / item.multiplier
elseif item.is_enum then
target[final_key] = item.enum_values[value]
else
target[final_key] = value
end
end
local function get_display_value(item, value)
if item.is_enum then
return item.enum_values[value] or "?"
end
if item.multiplier then
return value .. "%"
end
return tostring(value)
end
function Settings.load()
-- Check if pmem has been initialized
if pmem(PMEM_INIT_SLOT) ~= PMEM_INIT_VALUE then
-- First run - save defaults
Settings.save()
pmem(PMEM_INIT_SLOT, PMEM_INIT_VALUE)
return
end
-- Load values from pmem
for _, item in ipairs(SETTINGS_ITEMS) do
local stored = pmem(item.pmem_slot)
if stored >= item.min and stored <= item.max then
set_config_value(item, stored)
end
end
end
function Settings.save()
for _, item in ipairs(SETTINGS_ITEMS) do
local value = get_config_value(item)
pmem(item.pmem_slot, value)
end
end
function Settings.update()
cls(COLOR_BLACK)
UI.print_shadow("Settings", 85, 4, COLOR_BLUE, false, 2)
local start_y = 22
local item_height = 11
for i, item in ipairs(SETTINGS_ITEMS) do
local y = start_y + (i - 1) * item_height
local is_selected = (State.settings_selection == i)
local color = is_selected and COLOR_CYAN or COLOR_GRAY_LIGHT
-- Cursor
if is_selected then
print("<", 16, y, COLOR_CYAN)
print(">", 221, y, COLOR_CYAN)
end
-- Label
print(item.label, 26, y, color)
-- Value
local value = get_config_value(item)
local display = get_display_value(item, value)
print(display, 161, y, COLOR_YELLOW)
end
-- Back option
local back_y = start_y + #SETTINGS_ITEMS * item_height + 4
local back_selected = (State.settings_selection == #SETTINGS_ITEMS + 1)
if back_selected then
print(">", 71, back_y, COLOR_CYAN)
end
print("Save & Back", 81, back_y, back_selected and COLOR_CYAN or COLOR_GRAY_LIGHT)
-- Instructions at bottom
print("UP/DOWN:select LEFT/RIGHT:change", 28, 128, COLOR_GRAY_LIGHT)
-- Input handling
local max_selection = #SETTINGS_ITEMS + 1
if Input.up_pressed() then
State.settings_selection = State.settings_selection - 1
if State.settings_selection < 1 then State.settings_selection = max_selection end
elseif Input.down_pressed() then
State.settings_selection = State.settings_selection + 1
if State.settings_selection > max_selection then State.settings_selection = 1 end
elseif Input.left_pressed() and State.settings_selection <= #SETTINGS_ITEMS then
local item = SETTINGS_ITEMS[State.settings_selection]
local value = get_config_value(item)
value = value - item.step
if value < item.min then value = item.max end
set_config_value(item, value)
elseif Input.right_pressed() and State.settings_selection <= #SETTINGS_ITEMS then
local item = SETTINGS_ITEMS[State.settings_selection]
local value = get_config_value(item)
value = value + item.step
if value > item.max then value = item.min end
set_config_value(item, value)
elseif Input.action_pressed() or Input.back_pressed() then
Settings.save()
State.settings_selection = 1
State.game_state = GAME_STATE_MENU
end
end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- WinScreen module -- WinScreen module
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@@ -606,7 +980,7 @@ function Bomb.draw_explosions()
if expl.spread <= 0 then if expl.spread <= 0 then
rect(drawX, drawY, TILE_SIZE, TILE_SIZE, COLOR_RED) rect(drawX, drawY, TILE_SIZE, TILE_SIZE, COLOR_RED)
else else
local progress = 1 - (expl.spread / (expl.dist * SPREAD_DELAY)) local progress = 1 - (expl.spread / (expl.dist * Config.bomb.spread_delay))
if progress > 0 then if progress > 0 then
local size = math.floor(TILE_SIZE * progress) local size = math.floor(TILE_SIZE * progress)
local off = math.floor((TILE_SIZE - size) / 2) local off = math.floor((TILE_SIZE - size) / 2)
@@ -631,20 +1005,59 @@ function Bomb.place(player)
table.insert(State.bombs, { table.insert(State.bombs, {
x = bombX, x = bombX,
y = bombY, y = bombY,
timer = BOMB_TIMER, timer = Config.bomb.timer,
owner = player, owner = player,
power = player.bombPower power = player.bombPower
}) })
player.activeBombs = player.activeBombs + 1 player.activeBombs = player.activeBombs + 1
end end
local function spread_explosion(bombX, bombY, gridX, gridY, power, is_horizontal)
for _, dir in ipairs(SPREAD_DIRS) do
for dist = 1, power do
local explX, explY, eGridX, eGridY
if is_horizontal then
explX = bombX + dir * dist * TILE_SIZE
explY = bombY
eGridX = gridX + dir * dist
eGridY = gridY
if eGridX < 1 or eGridX > MAP_WIDTH then break end
else
explX = bombX
explY = bombY + dir * dist * TILE_SIZE
eGridX = gridX
eGridY = gridY + dir * dist
if eGridY < 1 or eGridY > MAP_HEIGHT then break end
end
local tile = State.map[eGridY][eGridX]
if tile == SOLID_WALL then break end
local is_breakable = tile == BREAKABLE_WALL
if is_breakable then
State.map[eGridY][eGridX] = EMPTY
end
table.insert(State.explosions, {
x = explX,
y = explY,
timer = Config.bomb.explosion_duration,
dist = dist,
spread = dist * Config.bomb.spread_delay
})
if is_breakable then break end
end
end
end
function Bomb.explode(bombX, bombY, power) function Bomb.explode(bombX, bombY, power)
power = power or 1 power = power or 1
sfx(0, nil, 30) Sound.play("explosion")
table.insert(State.explosions, { table.insert(State.explosions, {
x = bombX, x = bombX,
y = bombY, y = bombY,
timer = EXPLOSION_TIMER, timer = Config.bomb.explosion_duration,
dist = 0, dist = 0,
spread = 0 spread = 0
}) })
@@ -652,63 +1065,8 @@ function Bomb.explode(bombX, bombY, power)
local gridX = math.floor(bombX / TILE_SIZE) + 1 local gridX = math.floor(bombX / TILE_SIZE) + 1
local gridY = math.floor(bombY / TILE_SIZE) + 1 local gridY = math.floor(bombY / TILE_SIZE) + 1
-- horizontal explosion spread_explosion(bombX, bombY, gridX, gridY, power, true) -- horizontal
for _, dir in ipairs({-1, 1}) do spread_explosion(bombX, bombY, gridX, gridY, power, false) -- vertical
for dist = 1, power do
local explX = bombX + dir * dist * TILE_SIZE
local eGridX = gridX + dir * dist
if eGridX < 1 or eGridX > MAP_WIDTH then break end
local tile = State.map[gridY][eGridX]
if tile == SOLID_WALL then break end
if tile == BREAKABLE_WALL then
State.map[gridY][eGridX] = EMPTY
table.insert(State.explosions, {
x = explX,
y = bombY,
timer = EXPLOSION_TIMER,
dist = dist,
spread = dist * SPREAD_DELAY
})
break
end
table.insert(State.explosions, {
x = explX,
y = bombY,
timer = EXPLOSION_TIMER,
dist = dist,
spread = dist * SPREAD_DELAY
})
end
end
-- vertical explosion
for _, dir in ipairs({-1, 1}) do
for dist = 1, power do
local explY = bombY + dir * dist * TILE_SIZE
local eGridY = gridY + dir * dist
if eGridY < 1 or eGridY > MAP_HEIGHT then break end
local tile = State.map[eGridY][gridX]
if tile == SOLID_WALL then break end
if tile == BREAKABLE_WALL then
State.map[eGridY][gridX] = EMPTY
table.insert(State.explosions, {
x = bombX,
y = explY,
timer = EXPLOSION_TIMER,
dist = dist,
spread = dist * SPREAD_DELAY
})
break
end
table.insert(State.explosions, {
x = bombX,
y = explY,
timer = EXPLOSION_TIMER,
dist = dist,
spread = dist * SPREAD_DELAY
})
end
end
end end
function Bomb.update_all() function Bomb.update_all()
@@ -748,6 +1106,18 @@ end
-- AI module -- AI module
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
local function is_blast_line_blocked(pos1, pos2, fixedCoord, is_horizontal)
local minPos = math.min(pos1, pos2)
local maxPos = math.max(pos1, pos2)
for i = minPos + 1, maxPos - 1 do
local tile = is_horizontal and State.map[fixedCoord][i] or State.map[i][fixedCoord]
if tile == SOLID_WALL then
return true
end
end
return false
end
function AI.is_dangerous(gridX, gridY) function AI.is_dangerous(gridX, gridY)
-- Check active explosions -- Check active explosions
for _, expl in ipairs(State.explosions) do for _, expl in ipairs(State.explosions) do
@@ -758,44 +1128,31 @@ function AI.is_dangerous(gridX, gridY)
end end
end end
-- Check bombs about to explode (timer < 30) - need to escape! -- Check bombs about to explode - need to escape!
for _, bomb in ipairs(State.bombs) do for _, bomb in ipairs(State.bombs) do
local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1 local bombGridX = math.floor(bomb.x / TILE_SIZE) + 1
local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1 local bombGridY = math.floor(bomb.y / TILE_SIZE) + 1
local power = bomb.power or 1 local power = bomb.power or 1
-- Only urgent if bomb is about to explode -- Only urgent if bomb is about to explode
if bomb.timer < 30 then if bomb.timer < Config.ai.danger_threshold then
if gridX == bombGridX and gridY == bombGridY then if gridX == bombGridX and gridY == bombGridY then
return true return true
end end
-- Check blast radius only for soon-to-explode bombs -- Check horizontal blast radius
if gridY == bombGridY and math.abs(gridX - bombGridX) <= power then if gridY == bombGridY and math.abs(gridX - bombGridX) <= power then
local blocked = false if not is_blast_line_blocked(gridX, bombGridX, gridY, true) then
local minX = math.min(gridX, bombGridX) return true
local maxX = math.max(gridX, bombGridX)
for x = minX + 1, maxX - 1 do
if State.map[gridY][x] == SOLID_WALL then
blocked = true
break
end end
end end
if not blocked then return true end
end
-- Check vertical blast radius
if gridX == bombGridX and math.abs(gridY - bombGridY) <= power then if gridX == bombGridX and math.abs(gridY - bombGridY) <= power then
local blocked = false if not is_blast_line_blocked(gridY, bombGridY, gridX, false) then
local minY = math.min(gridY, bombGridY) return true
local maxY = math.max(gridY, bombGridY)
for y = minY + 1, maxY - 1 do
if State.map[y][gridX] == SOLID_WALL then
blocked = true
break
end end
end end
if not blocked then return true end
end
else else
-- For bombs with more time, just avoid the bomb cell itself -- For bombs with more time, just avoid the bomb cell itself
if gridX == bombGridX and gridY == bombGridY then if gridX == bombGridX and gridY == bombGridY then
@@ -898,7 +1255,10 @@ function AI.move_and_bomb(player, target)
local pwDist = math.abs(powerup.gridX - player.gridX) + math.abs(powerup.gridY - player.gridY) local pwDist = math.abs(powerup.gridX - player.gridX) + math.abs(powerup.gridY - player.gridY)
local targetDist = math.abs(target.gridX - player.gridX) + math.abs(target.gridY - player.gridY) local targetDist = math.abs(target.gridX - player.gridX) + math.abs(target.gridY - player.gridY)
if pwDist < targetDist or pwDist <= 5 then if pwDist < targetDist or pwDist <= 5 then
actualTarget = {gridX = powerup.gridX, gridY = powerup.gridY} actualTarget = {
gridX = powerup.gridX,
gridY = powerup.gridY
}
end end
end end
@@ -917,7 +1277,7 @@ function AI.move_and_bomb(player, target)
player.lastGridX = player.gridX player.lastGridX = player.gridX
player.lastGridY = player.gridY player.lastGridY = player.gridY
Bomb.place(player) Bomb.place(player)
player.bombCooldown = AI_BOMB_COOLDOWN player.bombCooldown = Config.ai.bomb_cooldown
AI.escape_from_bomb(player) AI.escape_from_bomb(player)
return return
end end
@@ -1009,7 +1369,7 @@ function AI.update(player, target)
end end
player.moveTimer = player.moveTimer + 1 player.moveTimer = player.moveTimer + 1
if player.moveTimer < AI_MOVE_DELAY then return end if player.moveTimer < Config.ai.move_delay then return end
player.moveTimer = 0 player.moveTimer = 0
AI.move_and_bomb(player, target) AI.move_and_bomb(player, target)
@@ -1033,9 +1393,9 @@ function Player.create(gridX, gridY, color, is_ai)
pixelX = (gridX - 1) * TILE_SIZE, pixelX = (gridX - 1) * TILE_SIZE,
pixelY = (gridY - 1) * TILE_SIZE, pixelY = (gridY - 1) * TILE_SIZE,
moving = false, moving = false,
maxBombs = 1, maxBombs = Config.player.start_bombs,
activeBombs = 0, activeBombs = 0,
bombPower = 1, bombPower = Config.player.start_power,
color = color, color = color,
is_ai = is_ai, is_ai = is_ai,
moveTimer = 0, moveTimer = 0,
@@ -1050,16 +1410,16 @@ function Player.update_movement(player)
local targetY = (player.gridY - 1) * TILE_SIZE local targetY = (player.gridY - 1) * TILE_SIZE
if player.pixelX < targetX then if player.pixelX < targetX then
player.pixelX = math.min(player.pixelX + MOVE_SPEED, targetX) player.pixelX = math.min(player.pixelX + Config.player.move_speed, targetX)
player.moving = true player.moving = true
elseif player.pixelX > targetX then elseif player.pixelX > targetX then
player.pixelX = math.max(player.pixelX - MOVE_SPEED, targetX) player.pixelX = math.max(player.pixelX - Config.player.move_speed, targetX)
player.moving = true player.moving = true
elseif player.pixelY < targetY then elseif player.pixelY < targetY then
player.pixelY = math.min(player.pixelY + MOVE_SPEED, targetY) player.pixelY = math.min(player.pixelY + Config.player.move_speed, targetY)
player.moving = true player.moving = true
elseif player.pixelY > targetY then elseif player.pixelY > targetY then
player.pixelY = math.max(player.pixelY - MOVE_SPEED, targetY) player.pixelY = math.max(player.pixelY - Config.player.move_speed, targetY)
player.moving = true player.moving = true
else else
player.moving = false player.moving = false
@@ -1119,9 +1479,9 @@ function Player.reset(player)
player.pixelX = (player.spawnX - 1) * TILE_SIZE player.pixelX = (player.spawnX - 1) * TILE_SIZE
player.pixelY = (player.spawnY - 1) * TILE_SIZE player.pixelY = (player.spawnY - 1) * TILE_SIZE
player.moving = false player.moving = false
player.maxBombs = 1 player.maxBombs = Config.player.start_bombs
player.activeBombs = 0 player.activeBombs = 0
player.bombPower = 1 player.bombPower = Config.player.start_power
player.bombCooldown = 0 player.bombCooldown = 0
end end
@@ -1158,7 +1518,7 @@ end
function Game.set_winner(player_num) function Game.set_winner(player_num)
State.winner = player_num State.winner = player_num
State.win_timer = WIN_SCREEN_DURATION State.win_timer = Config.timing.win_screen_duration
State.score[player_num] = State.score[player_num] + 1 State.score[player_num] = State.score[player_num] + 1
end end
@@ -1205,22 +1565,7 @@ end
-- Main game loop -- Main game loop
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
function TIC() local function update_playing()
if State.game_state == GAME_STATE_SPLASH then
Splash.update()
return
elseif State.game_state == GAME_STATE_MENU then
Menu.update()
return
elseif State.game_state == GAME_STATE_HELP then
Help.update()
return
elseif State.game_state == GAME_STATE_CREDITS then
Credits.update()
return
end
-- GAME_STATE_PLAYING
cls(COLOR_GREEN) cls(COLOR_GREEN)
-- ESC to return to menu -- ESC to return to menu
@@ -1243,6 +1588,22 @@ function TIC()
GameBoard.draw() GameBoard.draw()
end end
local STATE_HANDLERS = {
[GAME_STATE_SPLASH] = Splash.update,
[GAME_STATE_MENU] = Menu.update,
[GAME_STATE_HELP] = Help.update,
[GAME_STATE_CREDITS] = Credits.update,
[GAME_STATE_SETTINGS] = Settings.update,
[GAME_STATE_PLAYING] = update_playing,
}
function TIC()
local handler = STATE_HANDLERS[State.game_state]
if handler then
handler()
end
end
-- <TILES> -- <TILES>
-- 001:eccccccccc888888caaaaaaaca888888cacccccccacc0ccccacc0ccccacc0ccc -- 001:eccccccccc888888caaaaaaaca888888cacccccccacc0ccccacc0ccccacc0ccc
-- 002:ccccceee8888cceeaaaa0cee888a0ceeccca0ccc0cca0c0c0cca0c0c0cca0c0c -- 002:ccccceee8888cceeaaaa0cee888a0ceeccca0ccc0cca0c0c0cca0c0c0cca0c0c

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env python3
"""Simple static file server for Bomberman HTML export."""
import http.server
import socketserver
import os
import webbrowser
PORT = 3333
DIRECTORY = "bomberman"
os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), DIRECTORY))
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
url = f"http://localhost:{PORT}"
print(f"Serving at {url}")
webbrowser.open(url)
httpd.serve_forever()