18 Commits

Author SHA1 Message Date
Xuwznln
ee3c4bab7e Bump version: 0.0.5 → 0.0.6 2025-10-09 16:41:04 +08:00
Xuwznln
99524eee3d Add: elevator move event 2025-10-09 16:40:51 +08:00
Xuwznln
b2d03b2510 Bump version: 0.0.4 → 0.0.5 2025-10-06 15:16:47 +08:00
Xuwznln
1a8063e4fd fix performance calculation. fix floor error in approaching event. fix passenger board wrongly. 2025-10-06 15:16:35 +08:00
Xuwznln
692b853101 Update bug report template 2025-10-06 14:36:53 +08:00
Xuwznln
4c86816920 Update docs 2025-10-01 18:32:03 +08:00
Xuwznln
349511e00f Bump version: 0.0.3 → 0.0.4 2025-10-01 18:07:21 +08:00
Xuwznln
cbfae640d1 Bump version: 0.0.2 → 0.0.3 2025-10-01 18:00:14 +08:00
Xuwznln
de3fa68fa6 recover version 2025-10-01 18:00:10 +08:00
Xuwznln
78395349b2 Modify pypi package name 2025-10-01 17:59:35 +08:00
Xuwznln
2c19419ce8 Bump version: 0.0.1 → 0.0.2 2025-10-01 17:43:15 +08:00
Xuwznln
b9dcb259e3 Update ci 2025-10-01 17:28:57 +08:00
Xuwznln
1a8a0249c1 Add tests and docs 2025-10-01 17:23:30 +08:00
Xuwznln
a9fc374d31 Update ci 2025-10-01 17:07:31 +08:00
Xuwznln
e0a1e69fa8 added other examples 2025-09-29 10:44:14 +08:00
Xuwznln
d99b162d3a fix setup.py 2025-09-28 22:11:08 +08:00
Xuwznln
1090369863 bump actions/upload-artifact version 2025-09-28 20:33:23 +08:00
Xuwznln
7cae291860 try fix build 2025-09-28 20:30:54 +08:00
50 changed files with 8046 additions and 1575 deletions

10
.bumpversion.cfg Normal file
View File

@@ -0,0 +1,10 @@
[bumpversion]
current_version = 0.0.6
commit = True
tag = True
tag_name = v{new_version}
message = Bump version: {current_version} → {new_version}
[bumpversion:file:elevator_saga/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"

View File

@@ -0,0 +1,30 @@
---
name: Bug Report
about: Report a bug in the project
title: "[BUG] "
labels: bug
assignees: ""
---
**Version Information**
- elevator-py version: [e.g. v0.0.5]
- Python version: [e.g. 3.11]
**Bug Description**
A clear and concise description of the bug
**Steps to Reproduce**
1.
2.
3.
**Expected Behavior**
What you expected to happen
**Actual Behavior**
What actually happened
**Additional Context (Optional)**
Any other information that might help, such as error logs, screenshots, etc.

View File

@@ -18,13 +18,13 @@ updates:
commit-message:
prefix: "deps"
include: "scope"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
reviewers:
@@ -35,4 +35,3 @@ updates:
commit-message:
prefix: "ci"
include: "scope"

View File

@@ -1,220 +1,175 @@
name: CI
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ main, develop ]
branches: ["main", "dev"]
pull_request:
branches: [ main, develop ]
schedule:
# Run tests daily at 6 AM UTC
- cron: '0 6 * * *'
branches: ["main", "dev"]
jobs:
test:
# Step 1: Code formatting and pre-commit validation (fast failure)
code-format:
name: Code formatting and pre-commit validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
# Step 2: Basic build and test with minimum Python version (3.10)
basic-build:
name: Basic build (Python 3.10, Ubuntu)
runs-on: ubuntu-latest
needs: [code-format] # Only run after code formatting passes
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.10
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
ubuntu-pip-3.10-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
pip install -e .[dev]
- name: Test with pytest
run: |
pytest -v
- name: Verify documentation builds
run: |
pip install -e .[docs]
cd docs
make html
# Step 3: Security scan
security:
name: Security scan
runs-on: ubuntu-latest
needs: [basic-build] # Run in parallel with other tests after basic build
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Run Safety CLI to check for vulnerabilities
uses: pyupio/safety-action@v1
with:
api-key: ${{ secrets.SAFETY_CHECK }}
output-format: json
args: --detailed-output --output-format json
continue-on-error: true
- name: Upload security reports
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
safety-report.json
if: always()
# Step 4: Package build check
package-build:
name: Package build check
runs-on: ubuntu-latest
needs: [basic-build] # Run in parallel with other checks
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# Step 5: Full matrix build (only after all basic checks pass)
full-matrix-build:
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: [security, package-build] # Wait for all prerequisite checks
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
- name: Check dependencies
run: |
python run_all_tests.py --check-deps
- name: Run linting
run: |
black --check msgcenterpy tests
isort --check-only msgcenterpy tests
- name: Run type checking
run: |
mypy msgcenterpy
- name: Run tests with coverage
run: |
python -m pytest --cov=msgcenterpy --cov-report=xml --cov-report=term-missing
- name: Upload coverage to Codecov
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
python-version: ["3.10", "3.11", "3.12", "3.13"]
exclude:
# Skip the combination we already tested in basic-build
- os: ubuntu-latest
python-version: "3.10"
test-with-ros2:
name: Test with ROS2 (Ubuntu)
runs-on: ubuntu-latest
container:
image: ros:humble
steps:
- uses: actions/checkout@v4
- name: Set up Python
run: |
apt-get update
apt-get install -y python3-pip python3-dev
- name: Install ROS2 dependencies
run: |
apt-get update
apt-get install -y \
python3-rosidl-runtime-py \
python3-rclpy \
ros-humble-std-msgs \
ros-humble-geometry-msgs
- name: Install package
run: |
python3 -m pip install --upgrade pip
pip3 install -e .[dev,ros2]
- name: Run ROS2 tests
run: |
. /opt/ros/humble/setup.sh
python3 run_all_tests.py --type ros2
- name: Run conversion tests
run: |
. /opt/ros/humble/setup.sh
python3 run_all_tests.py --type conversion
- uses: actions/checkout@v5
build:
name: Build and check package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine check-manifest
- name: Check manifest
run: check-manifest
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
security:
name: Security scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install security tools
run: |
python -m pip install --upgrade pip
pip install bandit safety
- name: Run bandit security scan
run: bandit -r msgcenterpy/ -f json -o bandit-report.json
continue-on-error: true
- name: Run safety security scan
run: safety check --json --output safety-report.json
continue-on-error: true
- name: Upload security reports
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
if: always()
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
docs:
name: Build documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[docs]
# 为将来的文档构建预留
- name: Check documentation
run: |
echo "Documentation build placeholder"
# sphinx-build -b html docs docs/_build/html
performance:
name: Performance benchmarks
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest-benchmark
# 为将来的性能测试预留
- name: Run benchmarks
run: |
echo "Performance benchmarks placeholder"
# python -m pytest tests/benchmarks/ --benchmark-json=benchmark.json
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
pip install -e .[dev]
- name: Test with pytest
run: |
pytest

View File

@@ -1,269 +0,0 @@
name: Code Quality
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
schedule:
# Run weekly code quality checks
- cron: '0 2 * * 1'
jobs:
lint:
name: Linting and formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-lint-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-lint-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install flake8 pylint
- name: Run Black formatting check
run: |
black --check --diff elevator_saga tests
- name: Run isort import sorting check
run: |
isort --check-only --diff elevator_saga tests
- name: Run flake8
run: |
flake8 elevator_saga tests --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 elevator_saga tests --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Run pylint
run: |
pylint elevator_saga --exit-zero --output-format=parseable --reports=no | tee pylint-report.txt
- name: Upload lint reports
uses: actions/upload-artifact@v3
with:
name: lint-reports
path: |
pylint-report.txt
if: always()
type-check:
name: Type checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
- name: Run mypy type checking
run: |
mypy elevator_saga --html-report mypy-report --txt-report mypy-report
- name: Upload type check reports
uses: actions/upload-artifact@v3
with:
name: type-check-reports
path: mypy-report/
if: always()
complexity:
name: Code complexity analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install radon xenon
- name: Run cyclomatic complexity check
run: |
radon cc elevator_saga --min B --total-average
- name: Run maintainability index
run: |
radon mi elevator_saga --min B
- name: Run complexity with xenon
run: |
xenon --max-absolute B --max-modules B --max-average A elevator_saga
continue-on-error: true
dependencies:
name: Dependency analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pip-audit pipdeptree
- name: Generate dependency tree
run: |
pipdeptree --freeze > requirements-freeze.txt
pipdeptree --graph-output png > dependency-graph.png
- name: Check for known vulnerabilities
run: |
pip-audit --format=json --output=vulnerability-report.json
continue-on-error: true
- name: Upload dependency reports
uses: actions/upload-artifact@v3
with:
name: dependency-reports
path: |
requirements-freeze.txt
dependency-graph.png
vulnerability-report.json
if: always()
documentation:
name: Documentation quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pydocstyle interrogate
- name: Check docstring style
run: |
pydocstyle elevator_saga --count --explain --source
continue-on-error: true
- name: Check docstring coverage
run: |
interrogate elevator_saga --ignore-init-method --ignore-magic --ignore-module --ignore-nested-functions --fail-under=70
continue-on-error: true
pre-commit:
name: Pre-commit hooks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install pre-commit
run: |
python -m pip install --upgrade pip
pip install pre-commit
- name: Cache pre-commit
uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit hooks
run: |
pre-commit run --all-files --show-diff-on-failure
continue-on-error: true
summary:
name: Quality summary
runs-on: ubuntu-latest
needs: [lint, type-check, complexity, dependencies, documentation, pre-commit]
if: always()
steps:
- name: Create quality report
run: |
echo "## 📊 Code Quality Report" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
# Format results
if [ "${{ needs.lint.result }}" = "success" ]; then
echo "| Linting | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Linting | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.type-check.result }}" = "success" ]; then
echo "| Type Check | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Type Check | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.complexity.result }}" = "success" ]; then
echo "| Complexity | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Complexity | ⚠️ Warning |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.dependencies.result }}" = "success" ]; then
echo "| Dependencies | ✅ Secure |" >> $GITHUB_STEP_SUMMARY
else
echo "| Dependencies | ⚠️ Check needed |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.documentation.result }}" = "success" ]; then
echo "| Documentation | ✅ Good |" >> $GITHUB_STEP_SUMMARY
else
echo "| Documentation | ⚠️ Needs improvement |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.pre-commit.result }}" = "success" ]; then
echo "| Pre-commit | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Pre-commit | ⚠️ Some hooks failed |" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "📈 **Overall Quality:** ${{ needs.lint.result == 'success' && needs.type-check.result == 'success' && 'Good' || 'Needs attention' }}" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,34 +0,0 @@
name: Dependabot Auto-merge
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
dependabot:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment on major version updates
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
run: |
gh pr comment "$PR_URL" --body "🚨 **Major version update detected!** Please review this PR carefully before merging."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

83
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: Build and Deploy Documentation
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
branch:
description: "要部署文档的分支"
required: false
default: "main"
type: string
deploy_to_pages:
description: "是否部署到 GitHub Pages"
required: false
default: true
type: boolean
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build documentation
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch || github.ref }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# Install package in development mode to get version info
pip install -e .
# Install documentation dependencies
pip install -e .[docs]
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
- name: Build Sphinx documentation
run: |
cd docs
make html
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
with:
path: docs/_build/html
# Deploy to GitHub Pages
deploy:
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,184 +1,258 @@
name: Publish to PyPI
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload PyPI package
on:
release:
types: [published]
types: [published, edited]
workflow_dispatch:
inputs:
test_pypi:
description: 'Publish to Test PyPI instead of PyPI'
description: "Publish to Test PyPI instead of PyPI"
required: false
default: 'false'
default: false
type: boolean
permissions:
contents: read
jobs:
test:
name: Run tests before publish
# Step 1: Code formatting and pre-commit validation (fast failure)
code-format:
name: Code formatting and pre-commit validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
- name: Run comprehensive tests
run: |
python -m pytest --cov=msgcenterpy --cov-fail-under=80
- name: Run linting
run: |
black --check msgcenterpy tests
isort --check-only msgcenterpy tests
mypy msgcenterpy
build:
name: Build package
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
# Step 2: Basic build and test with minimum Python version (3.10)
basic-build:
name: Basic build (Python 3.10, Ubuntu)
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
# - name: Install build dependencies
# run: |
# python -m pip install --upgrade pip
# pip install build twine check-manifest
# - name: Verify version consistency
# run: |
# # 检查版本号一致性
# VERSION=$(python -c "import msgcenterpy; print(msgcenterpy.__version__)" 2>/dev/null || echo "unknown")
# TAG_VERSION="${GITHUB_REF#refs/tags/v}"
# if [ "$GITHUB_EVENT_NAME" = "release" ]; then
# if [ "$VERSION" != "$TAG_VERSION" ]; then
# echo "Version mismatch: package=$VERSION, tag=$TAG_VERSION"
# exit 1
# fi
# fi
# - name: Check manifest
# run: check-manifest
- name: Build package
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
retention-days: 30
needs: [code-format] # Only run after code formatting passes
publish-test:
name: Publish to Test PyPI
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.10
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
ubuntu-pip-3.10-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
pip install -e .[dev]
- name: Test with pytest
run: |
pytest -v
# Step 3: Security scan
security:
name: Security scan
runs-on: ubuntu-latest
needs: build
if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease)
environment:
name: test-pypi
url: https://test.pypi.org/p/msgcenterpy
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
verbose: true
needs: [basic-build] # Run in parallel with other tests after basic build
publish-pypi:
steps:
- uses: actions/checkout@v5
- name: Run Safety CLI to check for vulnerabilities
uses: pyupio/safety-action@v1
with:
api-key: ${{ secrets.SAFETY_CHECK }}
output-format: json
args: --detailed-output --output-format json
continue-on-error: true
- name: Upload security reports
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
safety-report.json
if: always()
release-build:
name: Build release distributions
runs-on: ubuntu-latest
needs: [basic-build]
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build twine
- name: Verify version consistency
if: github.event_name == 'release' && (github.event.action == 'published' || (github.event.action == 'edited' && !github.event.release.prerelease))
run: |
# Install package first
pip install -e .
# Get package version (fail fast if not available)
VERSION=$(python -c "import elevator_saga; print(elevator_saga.__version__)")
# Handle both v0.0.3 and 0.0.3 tag formats
RAW_TAG="${GITHUB_REF#refs/tags/}"
if [[ "$RAW_TAG" == v* ]]; then
TAG_VERSION="${RAW_TAG#v}"
else
TAG_VERSION="$RAW_TAG"
fi
echo "Package version: $VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$VERSION" != "$TAG_VERSION" ]; then
echo "❌ Version mismatch: package=$VERSION, tag=$TAG_VERSION"
echo "Please ensure the package version matches the git tag"
exit 1
fi
echo "✅ Version verification passed: $VERSION"
- name: Build release distributions
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
pypi-publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true'
environment:
name: pypi
url: https://pypi.org/p/msgcenterpy
needs:
- release-build
if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true' && (github.event.action == 'published' || github.event.action == 'edited')
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Note: For enhanced security, consider configuring deployment environments
# in your GitHub repository settings with protection rules
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
verbose: true
- name: Retrieve release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
test-pypi-publish:
name: Publish to Test PyPI
runs-on: ubuntu-latest
needs:
- release-build
if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease && (github.event.action == 'published' || github.event.action == 'edited'))
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Note: For enhanced security, consider configuring deployment environments
# in your GitHub repository settings with protection rules
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/
- name: Publish release distributions to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
create-github-release-assets:
name: Add assets to GitHub release
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
needs: release-build
if: github.event_name == 'release' && (github.event.action == 'published' || github.event.action == 'edited')
permissions:
contents: write # Need write access to upload release assets
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
- name: Upload release assets
uses: softprops/action-gh-release@v1
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Retrieve release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/
- name: Upload release assets
uses: softprops/action-gh-release@v2
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
post-publish:
name: Post-publish tasks
runs-on: ubuntu-latest
needs: [publish-pypi, publish-test]
if: always() && (needs.publish-pypi.result == 'success' || needs.publish-test.result == 'success')
steps:
- uses: actions/checkout@v4
- name: Create deployment summary
run: |
echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.publish-pypi.result }}" = "success" ]; then
echo "| PyPI | ✅ Published |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.publish-test.result }}" = "success" ]; then
echo "| Test PyPI | ✅ Published |" >> $GITHUB_STEP_SUMMARY
fi
echo "| GitHub Release | ✅ Assets uploaded |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY
# 为将来的通知预留
- name: Notify team
run: |
echo "Package published successfully!"
# 可以添加 Slack、Discord 等通知
needs: [pypi-publish, test-pypi-publish]
if: always() && (needs.pypi-publish.result == 'success' || needs.test-pypi-publish.result == 'success')
steps:
- uses: actions/checkout@v5
- name: Create deployment summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.pypi-publish.result }}" = "success" ]; then
echo "| PyPI | Published |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.test-pypi-publish.result }}" = "success" ]; then
echo "| Test PyPI | Published |" >> $GITHUB_STEP_SUMMARY
fi
echo "| GitHub Release | Assets uploaded |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY

65
.gitignore vendored
View File

@@ -1,5 +1,60 @@
.vscode
.idea
__pycache__
.mypy_cache
elevator_saga.egg-info
# ================================
# Python-related files
# ================================
# Compiled Python files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
dist/
*.egg-info/
.installed.cfg
*.egg
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Type checking
.mypy_cache/
# Documentation
docs/_build/
docs/_static/
docs/_templates/
docs/_static/
# ================================
# IDE and Editor files
# ================================
# PyCharm
.idea/
# Visual Studio Code
.vscode/
.cursor/
.cursorignore
pyrightconfig.json
# ================================
# Operating System files
# ================================
# macOS
.DS_Store

View File

@@ -5,33 +5,34 @@ repos:
hooks:
- id: black
language_version: python3
args: ["--line-length=88"]
args: ["--line-length=120"]
# Import sorting
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black", "--multi-line", "3"]
args:
["--profile", "black", "--multi-line", "3", "--line-length", "120"]
# Linting
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args:
- "--max-line-length=88"
- "--extend-ignore=E203,W503,F401"
- "--exclude=build,dist,.eggs"
args:
- "--max-line-length=200" # Allow longer lines after black formatting
- "--extend-ignore=E203,W503,F401,E402,E721,F841"
- "--exclude=build,dist,__pycache__,.mypy_cache,.pytest_cache,htmlcov,.idea,.vscode,docs/_build,elevatorpy.egg-info,elevator_saga.egg-info,.eggs"
# Type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-PyYAML]
args: ["--ignore-missing-imports", "--scripts-are-modules"]
exclude: "^(tests/|examples/)"
additional_dependencies: [types-PyYAML, types-jsonschema]
args: ["--ignore-missing-imports", "--disable-error-code=unused-ignore"]
files: "^(elevator_saga/)" # Check both source code
# General pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -45,18 +46,16 @@ repos:
- id: check-json
- id: check-toml
- id: check-xml
# Security
- id: check-merge-conflict
- id: check-case-conflict
- id: check-symlinks
- id: check-added-large-files
args: ["--maxkb=1000"]
# Python specific
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: debug-statements
- id: name-tests-test
args: ["--django"]
@@ -67,47 +66,17 @@ repos:
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
additional_dependencies: ["bandit[toml]", "pbr"]
exclude: "^tests/"
# Documentation
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
- id: pydocstyle
args: ["--convention=google"]
exclude: "^(tests/|examples/)"
# YAML/JSON formatting
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [yaml, json, markdown]
exclude: "^(.github/)"
# Spell checking (optional)
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
args: ["--write-changes"]
exclude: "^(.*\\.po|.*\\.pot|CHANGELOG\\.md)$"
exclude: "^(build/|dist/|__pycache__/|\\.mypy_cache/|\\.pytest_cache/|htmlcov/|\\.idea/|\\.vscode/|docs/_build/|elevatorpy\\.egg-info/|elevator_saga\\.egg-info/)"
# Global settings
default_stages: [commit, push]
default_stages: [pre-commit, pre-push]
fail_fast: false
# CI settings
ci:
autofix_commit_msg: |
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
autofix_prs: true
autoupdate_branch: ''
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
autoupdate_schedule: weekly
skip: []
submodules: false

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Elevator Saga Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# Elevator Saga
<div align="center">
[![PyPI version](https://badge.fury.io/py/elevator-py.svg)](https://badge.fury.io/py/elevator-py)
[![Python versions](https://img.shields.io/pypi/pyversions/elevator-py.svg)](https://pypi.org/project/elevator-py/)
[![Build Status](https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg)](https://github.com/ZGCA-Forge/Elevator/actions)
[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-brightgreen)](https://zgca-forge.github.io/Elevator/)
[![GitHub stars](https://img.shields.io/github/stars/ZGCA-Forge/Elevator.svg?style=social&label=Star)](https://github.com/ZGCA-Forge/Elevator)
[![GitHub forks](https://img.shields.io/github/forks/ZGCA-Forge/Elevator.svg?style=social&label=Fork)](https://github.com/ZGCA-Forge/Elevator/fork)
[![GitHub issues](https://img.shields.io/github/issues/ZGCA-Forge/Elevator.svg)](https://github.com/ZGCA-Forge/Elevator/issues)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ZGCA-Forge/Elevator/blob/main/LICENSE)
</div>
---
Elevator Saga is a Python implementation of an elevator [simulation game](https://play.elevatorsaga.com/) with a event-driven architecture Design and optimize elevator control algorithms to efficiently transport passengers in buildings.
### Features
- 🏢 **Realistic Simulation**: Physics-based elevator movement with acceleration, deceleration, and realistic timing
## Installation
### Basic Installation
```bash
pip install elevator-py
```
## Quick Start
### Running the Game
```bash
# Start the backend simulator (Terminal #1)
python -m elevator_saga.server.simulator
```
```bash
# Start your own client (Terminal #2)
# Example:
python -m elevator_saga.client_examples.bus_example
```
## Documentation
For detailed documentation, please visit: [https://zgca-forge.github.io/Elevator/](https://zgca-forge.github.io/Elevator/)
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=ZGCA-Forge/Elevator&type=Date)](https://star-history.com/#ZGCA-Forge/Elevator&Date)
## License
This project is licensed under MIT License - see the [LICENSE](LICENSE) file for details.
---
<div align="center">
Made with ❤️ by the ZGCA-Forge Team
</div>

3
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
_build/
_static/
_templates/

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

61
docs/README.md Normal file
View File

@@ -0,0 +1,61 @@
# Elevator Saga Documentation
This directory contains the Sphinx documentation for Elevator Saga.
## Building the Documentation
### Install Dependencies
```bash
pip install -r requirements.txt
```
### Build HTML Documentation
```bash
make html
```
The generated HTML documentation will be in `_build/html/`.
### View Documentation
Open `_build/html/index.html` in your browser:
```bash
# On Linux
xdg-open _build/html/index.html
# On macOS
open _build/html/index.html
# On Windows
start _build/html/index.html
```
### Other Build Formats
```bash
make latexpdf # Build PDF documentation
make epub # Build EPUB documentation
make clean # Clean build directory
```
## Documentation Structure
- `index.rst` - Main documentation index
- `models.rst` - Data models documentation
- `client.rst` - Client architecture and proxy models
- `communication.rst` - HTTP communication protocol
- `events.rst` - Event system and tick-based simulation
- `api/modules.rst` - Auto-generated API reference
## Contributing
When adding new documentation:
1. Create `.rst` files for new topics
2. Add them to the `toctree` in `index.rst`
3. Follow reStructuredText syntax
4. Build locally to verify formatting
5. Submit PR with documentation changes

39
docs/api/modules.rst Normal file
View File

@@ -0,0 +1,39 @@
API Reference
=============
.. toctree::
:maxdepth: 4
Core Modules
------------
.. automodule:: elevator_saga.core.models
:members:
:undoc-members:
:show-inheritance:
Client Modules
--------------
.. automodule:: elevator_saga.client.api_client
:members:
:undoc-members:
:show-inheritance:
.. automodule:: elevator_saga.client.proxy_models
:members:
:undoc-members:
:show-inheritance:
.. automodule:: elevator_saga.client.base_controller
:members:
:undoc-members:
:show-inheritance:
Server Modules
--------------
.. automodule:: elevator_saga.server.simulator
:members:
:undoc-members:
:show-inheritance:

351
docs/client.rst Normal file
View File

@@ -0,0 +1,351 @@
Client Architecture and Proxy Models
====================================
The Elevator Saga client provides a powerful abstraction layer that allows you to interact with the simulation using dynamic proxy objects. This architecture provides type-safe, read-only access to simulation state while enabling elevator control commands.
Overview
--------
The client architecture consists of three main components:
1. **Proxy Models** (``proxy_models.py``): Dynamic proxies that provide transparent access to server state
2. **API Client** (``api_client.py``): HTTP client for communicating with the server
3. **Base Controller** (``base_controller.py``): Abstract base class for implementing control algorithms
Proxy Models
------------
Proxy models in ``elevator_saga/client/proxy_models.py`` provide a clever way to access remote state as if it were local. They inherit from the data models but override attribute access to fetch fresh data from the server.
How Proxy Models Work
~~~~~~~~~~~~~~~~~~~~~~
The proxy pattern implementation uses Python's ``__getattribute__`` magic method to intercept attribute access:
1. When you access an attribute (e.g., ``elevator.current_floor``), the proxy intercepts the call
2. The proxy fetches the latest state from the server via API client
3. The proxy returns the requested attribute from the fresh state
4. All accesses are **read-only** to maintain consistency
This design ensures you always work with the most up-to-date simulation state without manual refresh calls.
ProxyElevator
~~~~~~~~~~~~~
Dynamic proxy for ``ElevatorState`` that provides access to elevator properties and control methods:
.. code-block:: python
class ProxyElevator(ElevatorState):
"""
Dynamic proxy for elevator state
Provides complete type-safe access and control methods
"""
def __init__(self, elevator_id: int, api_client: ElevatorAPIClient):
self._elevator_id = elevator_id
self._api_client = api_client
self._init_ok = True
def go_to_floor(self, floor: int, immediate: bool = False) -> bool:
"""Command elevator to go to specified floor"""
return self._api_client.go_to_floor(self._elevator_id, floor, immediate)
**Accessible Properties** (from ElevatorState):
- ``id``: Elevator identifier
- ``current_floor``: Current floor number
- ``current_floor_float``: Precise position (e.g., 2.5)
- ``target_floor``: Destination floor
- ``position``: Full Position object
- ``passengers``: List of passenger IDs on board
- ``max_capacity``: Maximum passenger capacity
- ``run_status``: Current ElevatorStatus
- ``target_floor_direction``: Direction to target (UP/DOWN/STOPPED)
- ``last_tick_direction``: Previous movement direction
- ``is_idle``: Whether stopped
- ``is_full``: Whether at capacity
- ``is_running``: Whether in motion
- ``pressed_floors``: Destination floors of current passengers
- ``load_factor``: Current load (0.0 to 1.0)
- ``indicators``: Up/down indicator lights
**Control Method**:
- ``go_to_floor(floor, immediate=False)``: Send elevator to floor
- ``immediate=True``: Change target immediately
- ``immediate=False``: Queue as next target after current destination
**Example Usage**:
.. code-block:: python
# Access elevator state
print(f"Elevator {elevator.id} at floor {elevator.current_floor}")
print(f"Direction: {elevator.target_floor_direction.value}")
print(f"Passengers: {len(elevator.passengers)}/{elevator.max_capacity}")
# Check status
if elevator.is_idle:
print("Elevator is idle")
elif elevator.is_full:
print("Elevator is full!")
# Control elevator
if elevator.current_floor == 0:
elevator.go_to_floor(5) # Send to floor 5
ProxyFloor
~~~~~~~~~~
Dynamic proxy for ``FloorState`` that provides access to floor information:
.. code-block:: python
class ProxyFloor(FloorState):
"""
Dynamic proxy for floor state
Provides read-only access to floor information
"""
def __init__(self, floor_id: int, api_client: ElevatorAPIClient):
self._floor_id = floor_id
self._api_client = api_client
self._init_ok = True
**Accessible Properties** (from FloorState):
- ``floor``: Floor number
- ``up_queue``: List of passenger IDs waiting to go up
- ``down_queue``: List of passenger IDs waiting to go down
- ``has_waiting_passengers``: Whether any passengers are waiting
- ``total_waiting``: Total number of waiting passengers
**Example Usage**:
.. code-block:: python
floor = floors[0]
print(f"Floor {floor.floor}")
print(f"Waiting to go up: {len(floor.up_queue)} passengers")
print(f"Waiting to go down: {len(floor.down_queue)} passengers")
if floor.has_waiting_passengers:
print(f"Total waiting: {floor.total_waiting}")
ProxyPassenger
~~~~~~~~~~~~~~
Dynamic proxy for ``PassengerInfo`` that provides access to passenger information:
.. code-block:: python
class ProxyPassenger(PassengerInfo):
"""
Dynamic proxy for passenger information
Provides read-only access to passenger data
"""
def __init__(self, passenger_id: int, api_client: ElevatorAPIClient):
self._passenger_id = passenger_id
self._api_client = api_client
self._init_ok = True
**Accessible Properties** (from PassengerInfo):
- ``id``: Passenger identifier
- ``origin``: Starting floor
- ``destination``: Target floor
- ``arrive_tick``: When passenger appeared
- ``pickup_tick``: When passenger boarded (0 if waiting)
- ``dropoff_tick``: When passenger reached destination (0 if in transit)
- ``elevator_id``: Current elevator ID (None if waiting)
- ``status``: Current PassengerStatus
- ``wait_time``: Ticks waited before boarding
- ``system_time``: Total ticks in system
- ``travel_direction``: UP or DOWN
**Example Usage**:
.. code-block:: python
print(f"Passenger {passenger.id}")
print(f"From floor {passenger.origin} to {passenger.destination}")
print(f"Status: {passenger.status.value}")
if passenger.status == PassengerStatus.IN_ELEVATOR:
print(f"In elevator {passenger.elevator_id}")
print(f"Waited {passenger.floor_wait_time} ticks")
Read-Only Protection
~~~~~~~~~~~~~~~~~~~~
All proxy models are **read-only**. Attempting to modify attributes will raise an error:
.. code-block:: python
elevator.current_floor = 5 # ❌ Raises AttributeError
elevator.passengers.append(123) # ❌ Raises AttributeError
This ensures that:
1. Client cannot corrupt server state
2. All state changes go through proper API commands
3. State consistency is maintained
Implementation Details
~~~~~~~~~~~~~~~~~~~~~~
The proxy implementation uses a clever pattern with ``_init_ok`` flag:
.. code-block:: python
class ProxyElevator(ElevatorState):
_init_ok = False
def __init__(self, elevator_id: int, api_client: ElevatorAPIClient):
self._elevator_id = elevator_id
self._api_client = api_client
self._init_ok = True # Enable proxy behavior
def __getattribute__(self, name: str) -> Any:
# During initialization, use normal attribute access
if not name.startswith("_") and self._init_ok and name not in self.__class__.__dict__:
# Try to find as a method of this class
try:
self_attr = object.__getattribute__(self, name)
if callable(self_attr):
return object.__getattribute__(self, name)
except AttributeError:
pass
# Fetch fresh state and return attribute
elevator_state = self._get_elevator_state()
return elevator_state.__getattribute__(name)
else:
return object.__getattribute__(self, name)
def __setattr__(self, name: str, value: Any) -> None:
# Allow setting during initialization only
if not self._init_ok:
object.__setattr__(self, name, value)
else:
raise AttributeError(f"Cannot modify read-only attribute '{name}'")
This design:
1. Allows normal initialization of internal fields (``_elevator_id``, ``_api_client``)
2. Intercepts access to data attributes after initialization
3. Preserves access to class methods (like ``go_to_floor``)
4. Blocks all attribute modifications after initialization
Base Controller
---------------
The ``ElevatorController`` class in ``base_controller.py`` provides the framework for implementing control algorithms:
.. code-block:: python
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from typing import List
class MyController(ElevatorController):
def __init__(self):
super().__init__("http://127.0.0.1:8000", auto_run=True)
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
"""Called once at start with all elevators and floors"""
print(f"Initialized with {len(elevators)} elevators")
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""Called when a passenger presses a button"""
print(f"Passenger {passenger.id} at floor {floor.floor} going {direction}")
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
"""Called when elevator stops at a floor"""
print(f"Elevator {elevator.id} stopped at floor {floor.floor}")
# Implement your dispatch logic here
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
"""Called when elevator becomes idle"""
# Send idle elevator somewhere useful
elevator.go_to_floor(0)
The controller provides these event handlers:
- ``on_init(elevators, floors)``: Initialization
- ``on_event_execute_start(tick, events, elevators, floors)``: Before processing tick events
- ``on_event_execute_end(tick, events, elevators, floors)``: After processing tick events
- ``on_passenger_call(passenger, floor, direction)``: Button press
- ``on_elevator_stopped(elevator, floor)``: Elevator arrival
- ``on_elevator_idle(elevator)``: Elevator becomes idle
- ``on_passenger_board(elevator, passenger)``: Passenger boards
- ``on_passenger_alight(elevator, passenger, floor)``: Passenger alights
- ``on_elevator_passing_floor(elevator, floor, direction)``: Elevator passes floor
- ``on_elevator_approaching(elevator, floor, direction)``: Elevator about to arrive
Complete Example
----------------
Here's a simple controller that sends idle elevators to the ground floor:
.. code-block:: python
#!/usr/bin/env python3
from typing import List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
class SimpleController(ElevatorController):
def __init__(self):
super().__init__("http://127.0.0.1:8000", auto_run=True)
self.pending_calls = []
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
print(f"Controlling {len(elevators)} elevators in {len(floors)}-floor building")
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
print(f"Call from floor {floor.floor}, direction {direction}")
self.pending_calls.append((floor.floor, direction))
# Dispatch nearest idle elevator
self._dispatch_to_call(floor.floor)
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
if self.pending_calls:
floor, direction = self.pending_calls.pop(0)
elevator.go_to_floor(floor)
else:
# No calls, return to ground floor
elevator.go_to_floor(0)
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
print(f"Elevator {elevator.id} at floor {floor.floor}")
print(f" Passengers on board: {len(elevator.passengers)}")
print(f" Waiting at floor: {floor.total_waiting}")
def _dispatch_to_call(self, floor: int) -> None:
# Find nearest idle elevator and send it
# (Simplified - real implementation would be more sophisticated)
pass
if __name__ == "__main__":
controller = SimpleController()
controller.start()
Benefits of Proxy Architecture
-------------------------------
1. **Type Safety**: IDE autocomplete and type checking work perfectly
2. **Always Fresh**: No need to manually refresh state
3. **Clean API**: Access remote state as if it were local
4. **Read-Only Safety**: Cannot accidentally corrupt server state
5. **Separation of Concerns**: State management handled by proxies, logic in controller
6. **Testability**: Can mock API client for unit tests
Next Steps
----------
- See :doc:`communication` for details on the HTTP API
- See :doc:`events` for understanding the event-driven simulation
- Check ``client_examples/bus_example.py`` for a complete implementation

527
docs/communication.rst Normal file
View File

@@ -0,0 +1,527 @@
HTTP Communication Architecture
================================
Elevator Saga uses a **client-server architecture** with HTTP-based communication. The server manages the simulation state and physics, while clients send commands and query state over HTTP.
Architecture Overview
---------------------
.. code-block:: text
┌─────────────────┐ ┌─────────────────┐
│ Client │ │ Server │
│ (Controller) │ │ (Simulator) │
├─────────────────┤ ├─────────────────┤
│ base_controller │◄──── HTTP ────────►│ simulator.py │
│ proxy_models │ │ Flask Routes │
│ api_client │ │ │
└─────────────────┘ └─────────────────┘
│ │
│ GET /api/state │
│ POST /api/step │
│ POST /api/elevators/:id/go_to_floor │
│ │
└───────────────────────────────────────┘
The separation provides:
- **Modularity**: Server and client can be developed independently
- **Multiple Clients**: Multiple controllers can compete/cooperate
- **Language Flexibility**: Clients can be written in any language
- **Network Deployment**: Server and client can run on different machines
Server Side: Simulator
----------------------
The server is implemented in ``elevator_saga/server/simulator.py`` using **Flask** as the HTTP framework.
API Endpoints
~~~~~~~~~~~~~
**GET /api/state**
Returns complete simulation state:
.. code-block:: python
@app.route("/api/state", methods=["GET"])
def get_state() -> Response:
try:
state = simulation.get_state()
return json_response(state)
except Exception as e:
return json_response({"error": str(e)}, 500)
Response format:
.. code-block:: json
{
"tick": 42,
"elevators": [
{
"id": 0,
"position": {"current_floor": 2, "target_floor": 5, "floor_up_position": 3},
"passengers": [101, 102],
"max_capacity": 10,
"run_status": "constant_speed",
"..."
}
],
"floors": [
{"floor": 0, "up_queue": [103], "down_queue": []},
"..."
],
"passengers": {
"101": {"id": 101, "origin": 0, "destination": 5, "..."}
},
"metrics": {
"done": 50,
"total": 100,
"avg_wait": 15.2,
"p95_wait": 30.0,
"avg_system": 25.5,
"p95_system": 45.0
}
}
**POST /api/step**
Advances simulation by specified number of ticks:
.. code-block:: python
@app.route("/api/step", methods=["POST"])
def step_simulation() -> Response:
try:
data = request.get_json() or {}
ticks = data.get("ticks", 1)
events = simulation.step(ticks)
return json_response({
"tick": simulation.tick,
"events": events,
})
except Exception as e:
return json_response({"error": str(e)}, 500)
Request body:
.. code-block:: json
{"ticks": 1}
Response:
.. code-block:: json
{
"tick": 43,
"events": [
{
"tick": 43,
"type": "stopped_at_floor",
"data": {"elevator": 0, "floor": 5, "reason": "move_reached"}
}
]
}
**POST /api/elevators/:id/go_to_floor**
Commands an elevator to go to a floor:
.. code-block:: python
@app.route("/api/elevators/<int:elevator_id>/go_to_floor", methods=["POST"])
def elevator_go_to_floor(elevator_id: int) -> Response:
try:
data = request.get_json() or {}
floor = data["floor"]
immediate = data.get("immediate", False)
simulation.elevator_go_to_floor(elevator_id, floor, immediate)
return json_response({"success": True})
except Exception as e:
return json_response({"error": str(e)}, 500)
Request body:
.. code-block:: json
{"floor": 5, "immediate": false}
- ``immediate=false``: Set as next target after current destination
- ``immediate=true``: Change target immediately (cancels current target)
**POST /api/reset**
Resets simulation to initial state:
.. code-block:: python
@app.route("/api/reset", methods=["POST"])
def reset_simulation() -> Response:
try:
simulation.reset()
return json_response({"success": True})
except Exception as e:
return json_response({"error": str(e)}, 500)
**POST /api/traffic/next**
Loads next traffic scenario:
.. code-block:: python
@app.route("/api/traffic/next", methods=["POST"])
def next_traffic_round() -> Response:
try:
full_reset = request.get_json().get("full_reset", False)
success = simulation.next_traffic_round(full_reset)
if success:
return json_response({"success": True})
else:
return json_response({"success": False, "error": "No more scenarios"}, 400)
except Exception as e:
return json_response({"error": str(e)}, 500)
**GET /api/traffic/info**
Gets current traffic scenario information:
.. code-block:: python
@app.route("/api/traffic/info", methods=["GET"])
def get_traffic_info() -> Response:
try:
info = simulation.get_traffic_info()
return json_response(info)
except Exception as e:
return json_response({"error": str(e)}, 500)
Response:
.. code-block:: json
{
"current_index": 0,
"total_files": 5,
"max_tick": 1000
}
Client Side: API Client
-----------------------
The client is implemented in ``elevator_saga/client/api_client.py`` using Python's built-in ``urllib`` library.
ElevatorAPIClient Class
~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
class ElevatorAPIClient:
"""Unified elevator API client"""
def __init__(self, base_url: str):
self.base_url = base_url.rstrip("/")
# Caching fields
self._cached_state: Optional[SimulationState] = None
self._cached_tick: int = -1
self._tick_processed: bool = False
State Caching Strategy
~~~~~~~~~~~~~~~~~~~~~~~
The API client implements **smart caching** to reduce HTTP requests:
.. code-block:: python
def get_state(self, force_reload: bool = False) -> SimulationState:
"""Get simulation state with caching"""
# Return cached state if valid
if not force_reload and self._cached_state is not None and not self._tick_processed:
return self._cached_state
# Fetch fresh state
response_data = self._send_get_request("/api/state")
# ... parse and create SimulationState ...
# Update cache
self._cached_state = simulation_state
self._cached_tick = simulation_state.tick
self._tick_processed = False # Mark as fresh
return simulation_state
def mark_tick_processed(self) -> None:
"""Mark current tick as processed, invalidating cache"""
self._tick_processed = True
**Cache Behavior**:
1. First ``get_state()`` call in a tick fetches from server
2. Subsequent calls within same tick return cached data
3. After ``step()`` is called, cache is invalidated
4. Next ``get_state()`` fetches fresh data
This provides:
- **Performance**: Minimize HTTP requests
- **Consistency**: All operations in a tick see same state
- **Freshness**: New tick always gets new state
Core API Methods
~~~~~~~~~~~~~~~~
**get_state(force_reload=False)**
Fetches current simulation state:
.. code-block:: python
def get_state(self, force_reload: bool = False) -> SimulationState:
if not force_reload and self._cached_state is not None and not self._tick_processed:
return self._cached_state
response_data = self._send_get_request("/api/state")
# Parse response into data models
elevators = [ElevatorState.from_dict(e) for e in response_data["elevators"]]
floors = [FloorState.from_dict(f) for f in response_data["floors"]]
# ... handle passengers and metrics ...
simulation_state = SimulationState(
tick=response_data["tick"],
elevators=elevators,
floors=floors,
passengers=passengers,
metrics=metrics,
events=[]
)
# Update cache
self._cached_state = simulation_state
self._cached_tick = simulation_state.tick
self._tick_processed = False
return simulation_state
**step(ticks=1)**
Advances simulation:
.. code-block:: python
def step(self, ticks: int = 1) -> StepResponse:
response_data = self._send_post_request("/api/step", {"ticks": ticks})
# Parse events
events = []
for event_data in response_data["events"]:
event_dict = event_data.copy()
if "type" in event_dict:
event_dict["type"] = EventType(event_dict["type"])
events.append(SimulationEvent.from_dict(event_dict))
return StepResponse(
success=True,
tick=response_data["tick"],
events=events
)
**go_to_floor(elevator_id, floor, immediate=False)**
Sends elevator to floor:
.. code-block:: python
def go_to_floor(self, elevator_id: int, floor: int, immediate: bool = False) -> bool:
command = GoToFloorCommand(
elevator_id=elevator_id,
floor=floor,
immediate=immediate
)
try:
response = self.send_elevator_command(command)
return response
except Exception as e:
debug_log(f"Go to floor failed: {e}")
return False
HTTP Request Implementation
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The client uses ``urllib.request`` for HTTP communication:
**GET Request**:
.. code-block:: python
def _send_get_request(self, endpoint: str) -> Dict[str, Any]:
url = f"{self.base_url}{endpoint}"
try:
with urllib.request.urlopen(url, timeout=60) as response:
data = json.loads(response.read().decode("utf-8"))
return data
except urllib.error.URLError as e:
raise RuntimeError(f"GET {url} failed: {e}")
**POST Request**:
.. code-block:: python
def _send_post_request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
url = f"{self.base_url}{endpoint}"
request_body = json.dumps(data).encode("utf-8")
req = urllib.request.Request(
url,
data=request_body,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=600) as response:
response_data = json.loads(response.read().decode("utf-8"))
return response_data
except urllib.error.URLError as e:
raise RuntimeError(f"POST {url} failed: {e}")
Communication Flow
------------------
Typical communication sequence during one tick:
.. code-block:: text
Client Server
│ │
│ 1. GET /api/state │
├─────────────────────────────────────►│
│ ◄── SimulationState (cached) │
│ │
│ 2. Analyze state, make decisions │
│ │
│ 3. POST /api/elevators/0/go_to_floor│
├─────────────────────────────────────►│
│ ◄── {"success": true} │
│ │
│ 4. GET /api/state (from cache) │
│ No HTTP request! │
│ │
│ 5. POST /api/step │
├─────────────────────────────────────►│
│ Server processes tick │
│ - Moves elevators │
│ - Boards/alights passengers │
│ - Generates events │
│ ◄── {tick: 43, events: [...]} │
│ │
│ 6. Process events │
│ Cache invalidated │
│ │
│ 7. GET /api/state (fetches fresh) │
├─────────────────────────────────────►│
│ ◄── SimulationState │
│ │
└──────────────────────────────────────┘
Error Handling
--------------
Both client and server implement robust error handling:
**Server Side**:
.. code-block:: python
@app.route("/api/step", methods=["POST"])
def step_simulation() -> Response:
try:
# ... process request ...
return json_response(result)
except Exception as e:
return json_response({"error": str(e)}, 500)
**Client Side**:
.. code-block:: python
def go_to_floor(self, elevator_id: int, floor: int, immediate: bool = False) -> bool:
try:
response = self.send_elevator_command(command)
return response
except Exception as e:
debug_log(f"Go to floor failed: {e}")
return False
Thread Safety
-------------
The simulator uses a lock to ensure thread-safe access:
.. code-block:: python
class ElevatorSimulation:
def __init__(self, ...):
self.lock = threading.Lock()
def step(self, num_ticks: int = 1) -> List[SimulationEvent]:
with self.lock:
# ... process ticks ...
def get_state(self) -> SimulationStateResponse:
with self.lock:
# ... return state ...
This allows Flask to handle concurrent requests safely.
**Batch Commands**:
.. code-block:: python
# ❌ Bad - sequential commands
elevator1.go_to_floor(5)
time.sleep(0.1) # Wait for response
elevator2.go_to_floor(3)
# ✅ Good - issue commands quickly
elevator1.go_to_floor(5)
elevator2.go_to_floor(3)
# All commands received before next tick
**Cache Awareness**:
Use ``mark_tick_processed()`` to explicitly invalidate cache if needed, but normally the framework handles this automatically.
Testing the API
---------------
You can test the API directly using curl:
.. code-block:: bash
# Get state
curl http://127.0.0.1:8000/api/state
# Step simulation
curl -X POST http://127.0.0.1:8000/api/step \
-H "Content-Type: application/json" \
-d '{"ticks": 1}'
# Send elevator to floor
curl -X POST http://127.0.0.1:8000/api/elevators/0/go_to_floor \
-H "Content-Type: application/json" \
-d '{"floor": 5, "immediate": false}'
# Reset simulation
curl -X POST http://127.0.0.1:8000/api/reset \
-H "Content-Type: application/json" \
-d '{}'
Next Steps
----------
- See :doc:`events` for understanding how events drive the simulation
- See :doc:`client` for using the API through proxy models
- Check the source code for complete implementation details

59
docs/conf.py Normal file
View File

@@ -0,0 +1,59 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Elevator Saga"
copyright = "2025, ZGCA-Forge Team"
author = "ZGCA-Forge Team"
release = "0.1.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx_rtd_theme",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
# -- Extension configuration -------------------------------------------------
# Napoleon settings
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = True
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_preprocess_types = False
napoleon_type_aliases = None
napoleon_attr_annotations = True
# Intersphinx mapping
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
}
# Todo extension
todo_include_todos = True

563
docs/events.rst Normal file
View File

@@ -0,0 +1,563 @@
Event-Driven Simulation and Tick-Based Execution
================================================
Elevator Saga uses an **event-driven, tick-based** discrete simulation model. The simulation progresses in discrete time steps (ticks), and events are generated to notify the controller about state changes.
Simulation Overview
-------------------
.. code-block:: text
┌──────────────────────────────────────────────────────────┐
│ Simulation Loop │
│ │
│ Tick N │
│ 1. Update elevator status (START_UP → CONSTANT_SPEED) │
│ 2. Process arrivals (new passengers) │
│ 3. Move elevators (physics simulation) │
│ 4. Process stops (boarding/alighting) │
│ 5. Generate events │
│ │
│ Events sent to client → Client processes → Commands │
│ │
│ Tick N+1 │
│ (repeat...) │
└──────────────────────────────────────────────────────────┘
Tick-Based Execution
--------------------
What is a Tick?
~~~~~~~~~~~~~~~
A **tick** is the fundamental unit of simulation time. Each tick represents one discrete time step where:
1. Physics is updated (elevators move)
2. State changes occur (passengers board/alight)
3. Events are generated
4. Controller receives events and makes decisions
Think of it like frames in a video game - the simulation updates at discrete intervals.
Tick Processing Flow
~~~~~~~~~~~~~~~~~~~~
In ``simulator.py``, the ``step()`` method processes ticks:
.. code-block:: python
def step(self, num_ticks: int = 1) -> List[SimulationEvent]:
"""Process one or more simulation ticks"""
with self.lock:
new_events = []
for _ in range(num_ticks):
self.state.tick += 1
tick_events = self._process_tick()
new_events.extend(tick_events)
# Force complete passengers if max duration reached
if self.tick >= self.max_duration_ticks:
completed_count = self.force_complete_remaining_passengers()
return new_events
Each ``_process_tick()`` executes the four-phase cycle:
.. code-block:: python
def _process_tick(self) -> List[SimulationEvent]:
"""Process one simulation tick"""
events_start = len(self.state.events)
# Phase 1: Update elevator status
self._update_elevator_status()
# Phase 2: Add new passengers from traffic queue
self._process_arrivals()
# Phase 3: Move elevators
self._move_elevators()
# Phase 4: Process elevator stops and passenger boarding/alighting
self._process_elevator_stops()
# Return events generated this tick
return self.state.events[events_start:]
Elevator State Machine
-----------------------
Elevators transition through states each tick:
.. code-block:: text
STOPPED ──(target set)──► START_UP ──(1 tick)──► CONSTANT_SPEED
(near target)
START_DOWN
(1 tick)
(arrived) STOPPED
State Transitions
~~~~~~~~~~~~~~~~~
**Phase 1: Update Elevator Status** (``_update_elevator_status()``):
.. code-block:: python
def _update_elevator_status(self) -> None:
"""Update elevator operational state"""
for elevator in self.elevators:
# If no direction, check for next target
if elevator.target_floor_direction == Direction.STOPPED:
if elevator.next_target_floor is not None:
self._set_elevator_target_floor(elevator, elevator.next_target_floor)
self._process_passenger_in()
elevator.next_target_floor = None
else:
continue
# Transition state machine
if elevator.run_status == ElevatorStatus.STOPPED:
# Start acceleration
elevator.run_status = ElevatorStatus.START_UP
elif elevator.run_status == ElevatorStatus.START_UP:
# Switch to constant speed after 1 tick
elevator.run_status = ElevatorStatus.CONSTANT_SPEED
**Important Notes**:
- ``START_UP`` = acceleration (not direction!)
- ``START_DOWN`` = deceleration (not direction!)
- Actual movement direction is ``target_floor_direction`` (UP/DOWN)
- State transitions happen **before** movement
Movement Physics
----------------
Speed by State
~~~~~~~~~~~~~~
Elevators move at different speeds depending on their state:
.. code-block:: python
def _move_elevators(self) -> None:
"""Move all elevators towards their destinations"""
for elevator in self.elevators:
# Determine speed based on state
movement_speed = 0
if elevator.run_status == ElevatorStatus.START_UP:
movement_speed = 1 # Accelerating: 0.1 floors/tick
elif elevator.run_status == ElevatorStatus.START_DOWN:
movement_speed = 1 # Decelerating: 0.1 floors/tick
elif elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
movement_speed = 2 # Full speed: 0.2 floors/tick
if movement_speed == 0:
continue
# Apply movement in appropriate direction
if elevator.target_floor_direction == Direction.UP:
new_floor = elevator.position.floor_up_position_add(movement_speed)
elif elevator.target_floor_direction == Direction.DOWN:
new_floor = elevator.position.floor_up_position_add(-movement_speed)
Position System
~~~~~~~~~~~~~~~
Positions use a **10-unit sub-floor** system:
- ``current_floor = 2, floor_up_position = 0`` → exactly at floor 2
- ``current_floor = 2, floor_up_position = 5`` → halfway between floors 2 and 3
- ``current_floor = 2, floor_up_position = 10`` → advances to ``current_floor = 3, floor_up_position = 0``
This granularity allows smooth movement and precise deceleration timing.
Deceleration Logic
~~~~~~~~~~~~~~~~~~
Elevators must decelerate before stopping:
.. code-block:: python
def _should_start_deceleration(self, elevator: ElevatorState) -> bool:
"""Check if should start decelerating"""
distance = self._calculate_distance_to_target(elevator)
return distance == 1 # Start deceleration 1 position unit before target
# In _move_elevators():
if elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
if self._should_start_deceleration(elevator):
elevator.run_status = ElevatorStatus.START_DOWN
This ensures elevators don't overshoot their target floor.
Event System
------------
Event Types
~~~~~~~~~~~
The simulation generates 9 types of events defined in ``EventType`` enum:
.. code-block:: python
class EventType(Enum):
UP_BUTTON_PRESSED = "up_button_pressed"
DOWN_BUTTON_PRESSED = "down_button_pressed"
PASSING_FLOOR = "passing_floor"
STOPPED_AT_FLOOR = "stopped_at_floor"
ELEVATOR_APPROACHING = "elevator_approaching"
IDLE = "idle"
PASSENGER_BOARD = "passenger_board"
PASSENGER_ALIGHT = "passenger_alight"
ELEVATOR_MOVE = "elevator_move"
Event Generation
~~~~~~~~~~~~~~~~
Events are generated during tick processing:
**Passenger Arrival**:
.. code-block:: python
def _process_arrivals(self) -> None:
"""Process new passenger arrivals"""
while self.traffic_queue and self.traffic_queue[0].tick <= self.tick:
traffic_entry = self.traffic_queue.pop(0)
passenger = PassengerInfo(
id=traffic_entry.id,
origin=traffic_entry.origin,
destination=traffic_entry.destination,
arrive_tick=self.tick,
)
self.passengers[passenger.id] = passenger
if passenger.destination > passenger.origin:
self.floors[passenger.origin].up_queue.append(passenger.id)
# Generate UP_BUTTON_PRESSED event
self._emit_event(
EventType.UP_BUTTON_PRESSED,
{"floor": passenger.origin, "passenger": passenger.id}
)
else:
self.floors[passenger.origin].down_queue.append(passenger.id)
# Generate DOWN_BUTTON_PRESSED event
self._emit_event(
EventType.DOWN_BUTTON_PRESSED,
{"floor": passenger.origin, "passenger": passenger.id}
)
**Elevator Movement**:
.. code-block:: python
def _move_elevators(self) -> None:
for elevator in self.elevators:
# ... movement logic ...
# Elevator moves
if elevator.target_floor_direction != Direction.STOPPED:
self._emit_event(
EventType.ELEVATOR_MOVE,
{
"elevator": elevator.id,
"from_position": old_position,
"to_position": elevator.position.current_floor_float,
"direction": elevator.target_floor_direction.value,
"status": elevator.run_status.value,
}
)
# Passing a floor
if old_floor != new_floor and new_floor != target_floor:
self._emit_event(
EventType.PASSING_FLOOR,
{
"elevator": elevator.id,
"floor": new_floor,
"direction": elevator.target_floor_direction.value
}
)
# About to arrive (during deceleration)
if self._near_next_stop(elevator):
self._emit_event(
EventType.ELEVATOR_APPROACHING,
{
"elevator": elevator.id,
"floor": int(round(elevator.position.current_floor_float)),
"direction": elevator.target_floor_direction.value
}
)
# Arrived at target
if target_floor == new_floor and elevator.position.floor_up_position == 0:
elevator.run_status = ElevatorStatus.STOPPED
self._emit_event(
EventType.STOPPED_AT_FLOOR,
{
"elevator": elevator.id,
"floor": new_floor,
"reason": "move_reached"
}
)
**Boarding and Alighting**:
.. code-block:: python
def _process_elevator_stops(self) -> None:
for elevator in self.elevators:
if elevator.run_status != ElevatorStatus.STOPPED:
continue
current_floor = elevator.current_floor
# Passengers alight
passengers_to_remove = []
for passenger_id in elevator.passengers:
passenger = self.passengers[passenger_id]
if passenger.destination == current_floor:
passenger.dropoff_tick = self.tick
passengers_to_remove.append(passenger_id)
for passenger_id in passengers_to_remove:
elevator.passengers.remove(passenger_id)
self._emit_event(
EventType.PASSENGER_ALIGHT,
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id}
)
**Idle Detection**:
.. code-block:: python
# If elevator stopped with no direction, it's idle
if elevator.last_tick_direction == Direction.STOPPED:
self._emit_event(
EventType.IDLE,
{"elevator": elevator.id, "floor": current_floor}
)
Event Processing in Controller
-------------------------------
The ``ElevatorController`` base class automatically routes events to handler methods:
.. code-block:: python
class ElevatorController(ABC):
def _execute_events(self, events: List[SimulationEvent]) -> None:
"""Process events and route to handlers"""
for event in events:
if event.type == EventType.UP_BUTTON_PRESSED:
passenger_id = event.data["passenger"]
floor = self.floors[event.data["floor"]]
passenger = ProxyPassenger(passenger_id, self.api_client)
self.on_passenger_call(passenger, floor, "up")
elif event.type == EventType.DOWN_BUTTON_PRESSED:
passenger_id = event.data["passenger"]
floor = self.floors[event.data["floor"]]
passenger = ProxyPassenger(passenger_id, self.api_client)
self.on_passenger_call(passenger, floor, "down")
elif event.type == EventType.STOPPED_AT_FLOOR:
elevator = self.elevators[event.data["elevator"]]
floor = self.floors[event.data["floor"]]
self.on_elevator_stopped(elevator, floor)
elif event.type == EventType.IDLE:
elevator = self.elevators[event.data["elevator"]]
self.on_elevator_idle(elevator)
elif event.type == EventType.ELEVATOR_MOVE:
elevator = self.elevators[event.data["elevator"]]
from_position = event.data["from_position"]
to_position = event.data["to_position"]
direction = event.data["direction"]
status = event.data["status"]
self.on_elevator_move(elevator, from_position, to_position, direction, status)
# ... other event types ...
Control Flow: Bus Example
--------------------------
The ``bus_example.py`` demonstrates a simple "bus route" algorithm:
.. code-block:: python
class ElevatorBusExampleController(ElevatorController):
def __init__(self):
super().__init__("http://127.0.0.1:8000", True)
self.all_passengers = []
self.max_floor = 0
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
"""Initialize elevators to starting positions"""
self.max_floor = floors[-1].floor
self.floors = floors
for i, elevator in enumerate(elevators):
# Distribute elevators evenly across floors
target_floor = (i * (len(floors) - 1)) // len(elevators)
elevator.go_to_floor(target_floor, immediate=True)
def on_event_execute_start(self, tick: int, events: List[SimulationEvent],
elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
"""Print state before processing events"""
print(f"Tick {tick}: Processing {len(events)} events {[e.type.value for e in events]}")
for elevator in elevators:
print(
f"\t{elevator.id}[{elevator.target_floor_direction.value},"
f"{elevator.current_floor_float}/{elevator.target_floor}]"
+ "👦" * len(elevator.passengers),
end=""
)
print()
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
"""Implement bus route logic"""
print(f"🛑 Elevator E{elevator.id} stopped at F{floor.floor}")
# Bus algorithm: go up to top, then down to bottom, repeat
if elevator.last_tick_direction == Direction.UP and elevator.current_floor == self.max_floor:
# At top, start going down
elevator.go_to_floor(elevator.current_floor - 1)
elif elevator.last_tick_direction == Direction.DOWN and elevator.current_floor == 0:
# At bottom, start going up
elevator.go_to_floor(elevator.current_floor + 1)
elif elevator.last_tick_direction == Direction.UP:
# Continue upward
elevator.go_to_floor(elevator.current_floor + 1)
elif elevator.last_tick_direction == Direction.DOWN:
# Continue downward
elevator.go_to_floor(elevator.current_floor - 1)
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
"""Send idle elevator to floor 1"""
elevator.go_to_floor(1)
Execution Sequence
~~~~~~~~~~~~~~~~~~
Here's what happens in a typical tick:
.. code-block:: text
Server: Tick 42
Phase 1: Update status
- Elevator 0: STOPPED → START_UP (has target)
Phase 2: Process arrivals
- Passenger 101 arrives at floor 0, going to floor 5
- Event: UP_BUTTON_PRESSED
Phase 3: Move elevators
- Elevator 0: floor 2.0 → 2.1 (accelerating)
Phase 4: Process stops
- (no stops this tick)
Events: [UP_BUTTON_PRESSED, PASSING_FLOOR]
Client: Receive events
on_event_execute_start(tick=42, events=[...])
- Print "Tick 42: Processing 2 events"
_execute_events():
- UP_BUTTON_PRESSED → on_passenger_call()
→ Controller decides which elevator to send
- PASSING_FLOOR → on_elevator_passing_floor()
on_event_execute_end(tick=42, events=[...])
Client: Send commands
- elevator.go_to_floor(0) → POST /api/elevators/0/go_to_floor
Client: Step simulation
- POST /api/step → Server processes tick 43
Key Timing Concepts
-------------------
Immediate vs. Queued
~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
# Queued (default): Wait until current target reached
elevator.go_to_floor(5, immediate=False)
# ← Sets elevator.next_target_floor = 5
# ← Processed when current_floor == target_floor
# Immediate: Change target right away
elevator.go_to_floor(5, immediate=True)
# ← Sets elevator.position.target_floor = 5 immediately
# ← May interrupt current journey
Use ``immediate=True`` for emergency redirects, ``immediate=False`` (default) for normal operation.
Performance Metrics
-------------------
Metrics are calculated from passenger data:
.. code-block:: python
def _calculate_metrics(self) -> PerformanceMetrics:
"""Calculate performance metrics"""
completed = [p for p in self.state.passengers.values()
if p.status == PassengerStatus.COMPLETED]
floor_wait_times = [float(p.floor_wait_time) for p in completed]
arrival_wait_times = [float(p.arrival_wait_time) for p in completed]
def average_excluding_top_percent(data: List[float], exclude_percent: int) -> float:
"""计算排除掉最长的指定百分比后的平均值"""
if not data:
return 0.0
sorted_data = sorted(data)
keep_count = int(len(sorted_data) * (100 - exclude_percent) / 100)
if keep_count == 0:
return 0.0
kept_data = sorted_data[:keep_count]
return sum(kept_data) / len(kept_data)
return PerformanceMetrics(
completed_passengers=len(completed),
total_passengers=len(self.state.passengers),
average_floor_wait_time=sum(floor_wait_times) / len(floor_wait_times) if floor_wait_times else 0,
p95_floor_wait_time=average_excluding_top_percent(floor_wait_times, 5),
average_arrival_wait_time=sum(arrival_wait_times) / len(arrival_wait_times) if arrival_wait_times else 0,
p95_arrival_wait_time=average_excluding_top_percent(arrival_wait_times, 5),
)
Key metrics:
- **Floor wait time**: ``pickup_tick - arrive_tick`` (在楼层等待的时间,从到达到上电梯)
- **Arrival wait time**: ``dropoff_tick - arrive_tick`` (总等待时间,从到达到下电梯)
- **P95 metrics**: 排除掉最长的5%时间后计算剩余95%的平均值
Summary
-------
The event-driven, tick-based architecture provides:
- **Deterministic**: Same inputs always produce same results
- **Testable**: Easy to create test scenarios with traffic files
- **Debuggable**: Clear event trail shows what happened when
- **Flexible**: Easy to implement different dispatch algorithms
- **Scalable**: Can simulate large buildings and many passengers
Next Steps
----------
- Study ``bus_example.py`` for a complete working example
- Implement your own controller by extending ``ElevatorController``
- Experiment with different dispatch algorithms
- Analyze performance metrics to optimize your approach

106
docs/index.rst Normal file
View File

@@ -0,0 +1,106 @@
Welcome to Elevator Saga's Documentation!
==========================================
.. image:: https://badge.fury.io/py/elevator-py.svg
:target: https://badge.fury.io/py/elevator-py
:alt: PyPI version
.. image:: https://img.shields.io/pypi/pyversions/elevator-py.svg
:target: https://pypi.org/project/elevator-py/
:alt: Python versions
.. image:: https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg
:target: https://github.com/ZGCA-Forge/Elevator/actions
:alt: Build Status
.. image:: https://img.shields.io/github/stars/ZGCA-Forge/Elevator.svg?style=social&label=Star
:target: https://github.com/ZGCA-Forge/Elevator
:alt: GitHub stars
Elevator Saga is a Python implementation of an elevator `simulation game <https://play.elevatorsaga.com/>`_ with an event-driven architecture. Design and optimize elevator control algorithms to efficiently transport passengers in buildings.
Features
--------
🏢 **Realistic Simulation**: Physics-based elevator movement with acceleration, deceleration, and realistic timing
🎮 **Event-Driven Architecture**: React to various events such as button presses, elevator arrivals, and passenger boarding
🔌 **Client-Server Model**: Separate simulation server from control logic for clean architecture
📊 **Performance Metrics**: Track wait times, system times, and completion rates
🎯 **Flexible Control**: Implement your own algorithms using a simple controller interface
Installation
------------
Basic Installation
~~~~~~~~~~~~~~~~~~
.. code-block:: bash
pip install elevator-py
Quick Start
-----------
Running the Simulation
~~~~~~~~~~~~~~~~~~~~~~
**Terminal #1: Start the backend simulator**
.. code-block:: bash
python -m elevator_saga.server.simulator
**Terminal #2: Start your controller**
.. code-block:: bash
python -m elevator_saga.client_examples.bus_example
Architecture Overview
---------------------
Elevator Saga follows a **client-server architecture**:
- **Server** (`simulator.py`): Manages the simulation state, physics, and event generation
- **Client** (`base_controller.py`): Implements control algorithms and reacts to events
- **Communication** (`api_client.py`): HTTP-based API for state queries and commands
- **Data Models** (`models.py`): Unified data structures shared between client and server
Contents
--------
.. toctree::
:maxdepth: 2
:caption: User Guide
models
client
communication
events
.. toctree::
:maxdepth: 1
:caption: API Reference
api/modules
Contributing
------------
Contributions are welcome! Please feel free to submit a Pull Request.
License
-------
This project is licensed under MIT License - see the LICENSE file for details.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

347
docs/models.rst Normal file
View File

@@ -0,0 +1,347 @@
Data Models
===========
The Elevator Saga system uses a unified data model architecture defined in ``elevator_saga/core/models.py``. These models ensure type consistency and serialization between the client and server components.
Overview
--------
All data models inherit from ``SerializableModel``, which provides:
- **to_dict()**: Convert model to dictionary
- **to_json()**: Convert model to JSON string
- **from_dict()**: Create model instance from dictionary
- **from_json()**: Create model instance from JSON string
This unified serialization approach ensures seamless data exchange over HTTP between client and server.
Core Enumerations
-----------------
Direction
~~~~~~~~~
Represents the direction of elevator movement or passenger travel:
.. code-block:: python
class Direction(Enum):
UP = "up" # Moving upward
DOWN = "down" # Moving downward
STOPPED = "stopped" # Not moving
ElevatorStatus
~~~~~~~~~~~~~~
Represents the elevator's operational state in the state machine:
.. code-block:: python
class ElevatorStatus(Enum):
START_UP = "start_up" # Acceleration phase
START_DOWN = "start_down" # Deceleration phase
CONSTANT_SPEED = "constant_speed" # Constant speed phase
STOPPED = "stopped" # Stopped at floor
**Important**: ``START_UP`` and ``START_DOWN`` refer to **acceleration/deceleration states**, not movement direction. The actual movement direction is determined by the ``target_floor_direction`` property.
State Machine Transition:
.. code-block:: text
STOPPED → START_UP → CONSTANT_SPEED → START_DOWN → STOPPED
1 tick 1 tick N ticks 1 tick
PassengerStatus
~~~~~~~~~~~~~~~
Represents the passenger's current state:
.. code-block:: python
class PassengerStatus(Enum):
WAITING = "waiting" # Waiting at origin floor
IN_ELEVATOR = "in_elevator" # Inside an elevator
COMPLETED = "completed" # Reached destination
CANCELLED = "cancelled" # Cancelled (unused)
EventType
~~~~~~~~~
Defines all possible simulation events:
.. code-block:: python
class EventType(Enum):
UP_BUTTON_PRESSED = "up_button_pressed"
DOWN_BUTTON_PRESSED = "down_button_pressed"
PASSING_FLOOR = "passing_floor"
STOPPED_AT_FLOOR = "stopped_at_floor"
ELEVATOR_APPROACHING = "elevator_approaching"
IDLE = "idle"
PASSENGER_BOARD = "passenger_board"
PASSENGER_ALIGHT = "passenger_alight"
Core Data Models
----------------
Position
~~~~~~~~
Represents elevator position with sub-floor granularity:
.. code-block:: python
@dataclass
class Position(SerializableModel):
current_floor: int = 0 # Current floor number
target_floor: int = 0 # Target floor number
floor_up_position: int = 0 # Position within floor (0-9)
- **floor_up_position**: Represents position between floors with 10 units per floor
- **current_floor_float**: Returns floating-point floor position (e.g., 2.5 = halfway between floors 2 and 3)
Example:
.. code-block:: python
position = Position(current_floor=2, floor_up_position=5)
print(position.current_floor_float) # 2.5
ElevatorState
~~~~~~~~~~~~~
Complete state information for an elevator:
.. code-block:: python
@dataclass
class ElevatorState(SerializableModel):
id: int
position: Position
next_target_floor: Optional[int] = None
passengers: List[int] = [] # Passenger IDs
max_capacity: int = 10
speed_pre_tick: float = 0.5
run_status: ElevatorStatus = ElevatorStatus.STOPPED
last_tick_direction: Direction = Direction.STOPPED
indicators: ElevatorIndicators = field(default_factory=ElevatorIndicators)
passenger_destinations: Dict[int, int] = {} # passenger_id -> floor
energy_consumed: float = 0.0
last_update_tick: int = 0
Key Properties:
- ``current_floor``: Integer floor number
- ``current_floor_float``: Precise position including sub-floor
- ``target_floor``: Destination floor
- ``target_floor_direction``: Direction to target (UP/DOWN/STOPPED)
- ``is_idle``: Whether elevator is stopped
- ``is_full``: Whether elevator is at capacity
- ``is_running``: Whether elevator is in motion
- ``pressed_floors``: List of destination floors for current passengers
- ``load_factor``: Current load as fraction of capacity (0.0 to 1.0)
FloorState
~~~~~~~~~~
State information for a building floor:
.. code-block:: python
@dataclass
class FloorState(SerializableModel):
floor: int
up_queue: List[int] = [] # Passenger IDs waiting to go up
down_queue: List[int] = [] # Passenger IDs waiting to go down
Properties:
- ``has_waiting_passengers``: Whether any passengers are waiting
- ``total_waiting``: Total number of waiting passengers
PassengerInfo
~~~~~~~~~~~~~
Complete information about a passenger:
.. code-block:: python
@dataclass
class PassengerInfo(SerializableModel):
id: int
origin: int # Starting floor
destination: int # Target floor
arrive_tick: int # When passenger appeared
pickup_tick: int = 0 # When passenger boarded elevator
dropoff_tick: int = 0 # When passenger reached destination
elevator_id: Optional[int] = None
Properties:
- ``status``: Current PassengerStatus
- ``wait_time``: Ticks waited before boarding
- ``system_time``: Total ticks in system (arrive to dropoff)
- ``travel_direction``: UP/DOWN based on origin and destination
SimulationState
~~~~~~~~~~~~~~~
Complete state of the simulation:
.. code-block:: python
@dataclass
class SimulationState(SerializableModel):
tick: int
elevators: List[ElevatorState]
floors: List[FloorState]
passengers: Dict[int, PassengerInfo]
metrics: PerformanceMetrics
events: List[SimulationEvent]
Helper Methods:
- ``get_elevator_by_id(id)``: Find elevator by ID
- ``get_floor_by_number(number)``: Find floor by number
- ``get_passengers_by_status(status)``: Filter passengers by status
- ``add_event(type, data)``: Add new event to queue
Traffic and Configuration
-------------------------
TrafficEntry
~~~~~~~~~~~~
Defines a single passenger arrival:
.. code-block:: python
@dataclass
class TrafficEntry(SerializableModel):
id: int
origin: int
destination: int
tick: int # When passenger arrives
TrafficPattern
~~~~~~~~~~~~~~
Collection of traffic entries defining a test scenario:
.. code-block:: python
@dataclass
class TrafficPattern(SerializableModel):
name: str
description: str
entries: List[TrafficEntry]
metadata: Dict[str, Any]
Properties:
- ``total_passengers``: Number of passengers in pattern
- ``duration``: Tick when last passenger arrives
Performance Metrics
-------------------
PerformanceMetrics
~~~~~~~~~~~~~~~~~~
Tracks simulation performance:
.. code-block:: python
@dataclass
class PerformanceMetrics(SerializableModel):
completed_passengers: int = 0
total_passengers: int = 0
average_floor_wait_time: float = 0.0
p95_floor_wait_time: float = 0.0 # 95th percentile
average_arrival_wait_time: float = 0.0
p95_arrival_wait_time: float = 0.0 # 95th percentile
Properties:
- ``completion_rate``: Fraction of passengers completed (0.0 to 1.0)
API Models
----------
The models also include HTTP API request/response structures:
- ``APIRequest``: Base request with ID and timestamp
- ``APIResponse``: Base response with success flag
- ``StepRequest/StepResponse``: Advance simulation time
- ``StateRequest``: Query simulation state
- ``ElevatorCommand``: Send command to elevator
- ``GoToFloorCommand``: Specific command to move elevator
Example Usage
-------------
Creating a Simulation State
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from elevator_saga.core.models import (
create_empty_simulation_state,
ElevatorState,
Position,
)
# Create a building with 3 elevators, 10 floors, capacity 8
state = create_empty_simulation_state(
elevators=3,
floors=10,
max_capacity=8
)
# Access elevator state
elevator = state.elevators[0]
print(f"Elevator {elevator.id} at floor {elevator.current_floor}")
Working with Traffic Patterns
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from elevator_saga.core.models import (
create_simple_traffic_pattern,
TrafficPattern,
)
# Create traffic pattern: (origin, destination, tick)
pattern = create_simple_traffic_pattern(
name="morning_rush",
passengers=[
(0, 5, 10), # Floor 0→5 at tick 10
(0, 8, 15), # Floor 0→8 at tick 15
(2, 0, 20), # Floor 2→0 at tick 20
]
)
print(f"Pattern has {pattern.total_passengers} passengers")
print(f"Duration: {pattern.duration} ticks")
Serialization
~~~~~~~~~~~~~
All models support JSON serialization:
.. code-block:: python
# Serialize to JSON
elevator = state.elevators[0]
json_str = elevator.to_json()
# Deserialize from JSON
restored = ElevatorState.from_json(json_str)
# Or use dictionaries
data = elevator.to_dict()
restored = ElevatorState.from_dict(data)
This enables seamless transmission over HTTP between client and server.

2
docs/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
sphinx>=5.0.0
sphinx-rtd-theme>=1.0.0

View File

@@ -6,5 +6,5 @@ A Python implementation of the Elevator Saga game with event-driven architecture
realistic elevator dispatch algorithm development and testing.
"""
__version__ = "1.0.0"
__version__ = "0.0.6"
__author__ = "ZGCA Team"

View File

@@ -6,7 +6,7 @@ Unified API Client for Elevator Saga
import json
import urllib.error
import urllib.request
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional
from elevator_saga.core.models import (
ElevatorState,
@@ -14,7 +14,6 @@ from elevator_saga.core.models import (
GoToFloorCommand,
PassengerInfo,
PerformanceMetrics,
SetIndicatorsCommand,
SimulationEvent,
SimulationState,
StepResponse,
@@ -64,16 +63,8 @@ class ElevatorAPIClient:
# 使用服务端返回的metrics数据
metrics_data = response_data.get("metrics", {})
if metrics_data:
# 转换为PerformanceMetrics格式
metrics = PerformanceMetrics(
completed_passengers=metrics_data.get("done", 0),
total_passengers=metrics_data.get("total", 0),
average_wait_time=metrics_data.get("avg_wait", 0),
p95_wait_time=metrics_data.get("p95_wait", 0),
average_system_time=metrics_data.get("avg_system", 0),
p95_system_time=metrics_data.get("p95_system", 0),
# total_energy_consumption=metrics_data.get("energy_total", 0),
)
# 直接从字典创建PerformanceMetrics对象
metrics = PerformanceMetrics.from_dict(metrics_data)
else:
metrics = PerformanceMetrics()
@@ -132,15 +123,17 @@ class ElevatorAPIClient:
else:
raise RuntimeError(f"Step failed: {response_data.get('error')}")
def send_elevator_command(self, command: Union[GoToFloorCommand, SetIndicatorsCommand]) -> bool:
def send_elevator_command(self, command: GoToFloorCommand) -> bool:
"""发送电梯命令"""
endpoint = self._get_elevator_endpoint(command)
debug_log(f"Sending elevator command: {command.command_type} to elevator {command.elevator_id} To:F{command.floor}")
debug_log(
f"Sending elevator command: {command.command_type} to elevator {command.elevator_id} To:F{command.floor}"
)
response_data = self._send_post_request(endpoint, command.parameters)
if response_data.get("success"):
return response_data["success"]
return bool(response_data["success"])
else:
raise RuntimeError(f"Command failed: {response_data.get('error_message')}")
@@ -155,25 +148,12 @@ class ElevatorAPIClient:
debug_log(f"Go to floor failed: {e}")
return False
def set_indicators(self, elevator_id: int, up: Optional[bool] = None, down: Optional[bool] = None) -> bool:
"""设置电梯指示灯"""
command = SetIndicatorsCommand(elevator_id=elevator_id, up=up, down=down)
try:
response = self.send_elevator_command(command)
return response
except Exception as e:
debug_log(f"Set indicators failed: {e}")
return False
def _get_elevator_endpoint(self, command: Union[GoToFloorCommand, SetIndicatorsCommand]) -> str:
def _get_elevator_endpoint(self, command: GoToFloorCommand) -> str:
"""获取电梯命令端点"""
base = f"/api/elevators/{command.elevator_id}"
if isinstance(command, GoToFloorCommand):
return f"{base}/go_to_floor"
else: # SetIndicatorsCommand
return f"{base}/set_indicators"
def _send_get_request(self, endpoint: str) -> Dict[str, Any]:
"""发送GET请求"""
@@ -182,7 +162,7 @@ class ElevatorAPIClient:
try:
with urllib.request.urlopen(url, timeout=60) as response:
data = json.loads(response.read().decode("utf-8"))
data: Dict[str, Any] = json.loads(response.read().decode("utf-8"))
# debug_log(f"GET {url} -> {response.status}")
return data
except urllib.error.URLError as e:
@@ -192,7 +172,7 @@ class ElevatorAPIClient:
"""重置模拟"""
try:
response_data = self._send_post_request("/api/reset", {})
success = response_data.get("success", False)
success = bool(response_data.get("success", False))
if success:
# 清空缓存,因为状态已重置
self._cached_state = None
@@ -204,11 +184,11 @@ class ElevatorAPIClient:
debug_log(f"Reset failed: {e}")
return False
def next_traffic_round(self, full_reset = False) -> bool:
def next_traffic_round(self, full_reset: bool = False) -> bool:
"""切换到下一个流量文件"""
try:
response_data = self._send_post_request("/api/traffic/next", {"full_reset": full_reset})
success = response_data.get("success", False)
success = bool(response_data.get("success", False))
if success:
# 清空缓存,因为流量文件已切换,状态会改变
self._cached_state = None
@@ -244,7 +224,7 @@ class ElevatorAPIClient:
try:
with urllib.request.urlopen(req, timeout=600) as response:
response_data = json.loads(response.read().decode("utf-8"))
response_data: Dict[str, Any] = json.loads(response.read().decode("utf-8"))
# debug_log(f"POST {url} -> {response.status}")
return response_data
except urllib.error.URLError as e:

View File

@@ -7,7 +7,7 @@ import os
import time
from abc import ABC, abstractmethod
from pprint import pprint
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List
from elevator_saga.client.api_client import ElevatorAPIClient
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
@@ -44,7 +44,7 @@ class ElevatorController(ABC):
self.api_client = ElevatorAPIClient(server_url)
@abstractmethod
def on_init(self, elevators: List[Any], floors: List[Any]):
def on_init(self, elevators: List[Any], floors: List[Any]) -> None:
"""
算法初始化方法 - 必须由子类实现
@@ -55,7 +55,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_event_execute_start(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]):
def on_event_execute_start(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]) -> None:
"""
事件执行前的回调 - 必须由子类实现
@@ -68,7 +68,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_event_execute_end(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]):
def on_event_execute_end(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]) -> None:
"""
事件执行后的回调 - 必须由子类实现
@@ -80,20 +80,20 @@ class ElevatorController(ABC):
"""
pass
def on_start(self):
def on_start(self) -> None:
"""
算法启动前的回调 - 可选实现
"""
print(f"启动 {self.__class__.__name__} 算法")
def on_stop(self):
def on_stop(self) -> None:
"""
算法停止后的回调 - 可选实现
"""
print(f"停止 {self.__class__.__name__} 算法")
@abstractmethod
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str):
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""
乘客呼叫时的回调 - 可选实现
@@ -104,7 +104,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_idle(self, elevator: ProxyElevator):
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
"""
电梯空闲时的回调 - 可选实现
@@ -114,7 +114,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor):
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
"""
电梯停靠时的回调 - 可选实现
@@ -125,7 +125,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger):
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger) -> None:
"""
乘客上梯时的回调 - 可选实现
@@ -136,7 +136,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor):
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
"""
乘客下车时的回调 - 可选实现
@@ -148,7 +148,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str):
def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
"""
电梯经过楼层时的回调 - 可选实现
@@ -160,7 +160,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str):
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
"""
电梯即将到达时的回调 - 可选实现
@@ -171,7 +171,23 @@ class ElevatorController(ABC):
"""
pass
def _internal_init(self, elevators: List[Any], floors: List[Any]):
@abstractmethod
def on_elevator_move(
self, elevator: ProxyElevator, from_position: float, to_position: float, direction: str, status: str
) -> None:
"""
电梯移动时的回调 - 可选实现
Args:
elevator: 电梯代理对象
from_position: 起始位置(浮点数表示楼层)
to_position: 目标位置(浮点数表示楼层)
direction: 移动方向
status: 电梯运行状态
"""
pass
def _internal_init(self, elevators: List[Any], floors: List[Any]) -> None:
"""内部初始化方法"""
self.elevators = elevators
self.floors = floors
@@ -180,7 +196,7 @@ class ElevatorController(ABC):
# 调用用户的初始化方法
self.on_init(elevators, floors)
def start(self):
def start(self) -> None:
"""
启动控制器
"""
@@ -198,12 +214,12 @@ class ElevatorController(ABC):
self.is_running = False
self.on_stop()
def stop(self):
def stop(self) -> None:
"""停止控制器"""
self.is_running = False
print(f"停止 {self.__class__.__name__}")
def on_simulation_complete(self, final_state: Dict[str, Any]):
def on_simulation_complete(self, final_state: Dict[str, Any]) -> None:
"""
模拟完成时的回调 - 可选实现
@@ -212,13 +228,13 @@ class ElevatorController(ABC):
"""
pass
def _run_event_driven_simulation(self):
def _run_event_driven_simulation(self) -> None:
"""运行事件驱动的模拟"""
try:
# 获取初始状态并初始化默认从0开始
try:
state = self.api_client.get_state()
except ConnectionResetError as ex:
except ConnectionResetError as _: # noqa: F841
print(f"模拟器可能并没有开启,请检查模拟器是否启动 {self.api_client.base_url}")
os._exit(1)
if state.tick > 0:
@@ -304,7 +320,7 @@ class ElevatorController(ABC):
try:
traffic_info = self.api_client.get_traffic_info()
if traffic_info:
self.current_traffic_max_tick = traffic_info["max_tick"]
self.current_traffic_max_tick = int(traffic_info["max_tick"])
debug_log(f"Updated traffic info - max_tick: {self.current_traffic_max_tick}")
else:
debug_log("Failed to get traffic info")
@@ -313,7 +329,7 @@ class ElevatorController(ABC):
debug_log(f"Error updating traffic info: {e}")
self.current_traffic_max_tick = 0
def _handle_single_event(self, event: SimulationEvent):
def _handle_single_event(self, event: SimulationEvent) -> None:
"""处理单个事件"""
if event.type == EventType.UP_BUTTON_PRESSED:
floor_id = event.data["floor"]
@@ -352,9 +368,7 @@ class ElevatorController(ABC):
if elevator_id is not None and floor_id is not None and direction is not None:
elevator_proxy = ProxyElevator(elevator_id, self.api_client)
floor_proxy = ProxyFloor(floor_id, self.api_client)
# 服务端发送的direction是字符串直接使用
direction_str = direction if isinstance(direction, str) else direction.value
self.on_elevator_passing_floor(elevator_proxy, floor_proxy, direction_str)
self.on_elevator_passing_floor(elevator_proxy, floor_proxy, direction)
elif event.type == EventType.ELEVATOR_APPROACHING:
elevator_id = event.data.get("elevator")
@@ -363,9 +377,7 @@ class ElevatorController(ABC):
if elevator_id is not None and floor_id is not None and direction is not None:
elevator_proxy = ProxyElevator(elevator_id, self.api_client)
floor_proxy = ProxyFloor(floor_id, self.api_client)
# 服务端发送的direction是字符串直接使用
direction_str = direction if isinstance(direction, str) else direction.value
self.on_elevator_approaching(elevator_proxy, floor_proxy, direction_str)
self.on_elevator_approaching(elevator_proxy, floor_proxy, direction)
elif event.type == EventType.PASSENGER_BOARD:
elevator_id = event.data.get("elevator")
@@ -385,7 +397,23 @@ class ElevatorController(ABC):
floor_proxy = ProxyFloor(floor_id, self.api_client)
self.on_passenger_alight(elevator_proxy, passenger_proxy, floor_proxy)
def _reset_and_reinit(self):
elif event.type == EventType.ELEVATOR_MOVE:
elevator_id = event.data.get("elevator")
from_position = event.data.get("from_position")
to_position = event.data.get("to_position")
direction = event.data.get("direction")
status = event.data.get("status")
if (
elevator_id is not None
and from_position is not None
and to_position is not None
and direction is not None
and status is not None
):
elevator_proxy = ProxyElevator(elevator_id, self.api_client)
self.on_elevator_move(elevator_proxy, from_position, to_position, direction, status)
def _reset_and_reinit(self) -> None:
"""重置并重新初始化"""
try:
# 重置服务器状态

View File

@@ -10,48 +10,38 @@ class ProxyFloor(FloorState):
直接使用 FloorState 数据模型实例,提供完整的类型安全访问
"""
init_ok = False
_init_ok = False
def __init__(self, floor_id: int, api_client: ElevatorAPIClient):
self._floor_id = floor_id
self._api_client = api_client
self._cached_instance = None
self.init_ok = True
self._init_ok = True
def _get_floor_state(self) -> FloorState:
"""获取 FloorState 实例"""
# 获取当前状态
state = self._api_client.get_state()
floor_data = next((f for f in state.floors if f.floor == self._floor_id), None)
if floor_data is None:
raise AttributeError(f"Floor {self._floor_id} not found")
raise ValueError(f"Floor {self._floor_id} not found in state")
return floor_data
# 如果是字典,转换为 FloorState 实例
if isinstance(floor_data, dict):
return FloorState.from_dict(floor_data)
def __getattribute__(self, name: str) -> Any:
if not name.startswith("_") and self._init_ok and name not in self.__class__.__dict__:
try:
self_attr = object.__getattribute__(self, name)
if callable(self_attr):
return object.__getattribute__(self, name)
except AttributeError:
pass
floor_state = self._get_floor_state()
return floor_state.__getattribute__(name)
else:
# 如果已经是 FloorState 实例,直接返回
return floor_data
def __getattr__(self, name: str) -> Any:
"""动态获取楼层属性"""
floor_state = self._get_floor_state()
try:
if hasattr(floor_state, name):
attr = getattr(floor_state, name)
# 如果是 property 或方法,调用并返回结果
if callable(attr):
return attr()
else:
return attr
except AttributeError:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
return object.__getattribute__(self, name)
def __setattr__(self, name: str, value: Any) -> None:
"""禁止修改属性,保持只读特性"""
if not self.init_ok:
if not self._init_ok:
object.__setattr__(self, name, value)
else:
raise AttributeError(f"Cannot modify read-only attribute '{name}'")
@@ -66,54 +56,42 @@ class ProxyElevator(ElevatorState):
直接使用 ElevatorState 数据模型实例,提供完整的类型安全访问和操作方法
"""
init_ok = False
_init_ok = False
def __init__(self, elevator_id: int, api_client: ElevatorAPIClient):
self._elevator_id = elevator_id
self._api_client = api_client
self.init_ok = True
self._init_ok = True
def _get_elevator_state(self) -> ElevatorState:
"""获取 ElevatorState 实例"""
# 获取当前状态
state = self._api_client.get_state()
elevator_data = next((e for e in state.elevators if e.id == self._elevator_id), None)
if elevator_data is None:
raise AttributeError(f"Elevator {self._elevator_id} not found")
raise ValueError(f"Elevator {self._elevator_id} not found in state")
return elevator_data
# 如果是字典,转换为 ElevatorState 实例
if isinstance(elevator_data, dict):
return ElevatorState.from_dict(elevator_data)
else:
# 如果已经是 ElevatorState 实例,直接返回
return elevator_data
def __getattr__(self, name: str) -> Any:
"""动态获取电梯属性"""
try:
def __getattribute__(self, name: str) -> Any:
if not name.startswith("_") and self._init_ok and name not in self.__class__.__dict__:
try:
self_attr = object.__getattribute__(self, name)
if callable(self_attr):
return object.__getattribute__(self, name)
except AttributeError:
pass
elevator_state = self._get_elevator_state()
# 直接从 ElevatorState 实例获取属性
return getattr(elevator_state, name)
except AttributeError:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
return elevator_state.__getattribute__(name)
else:
return object.__getattribute__(self, name)
def go_to_floor(self, floor: int, immediate: bool = False) -> bool:
"""前往指定楼层"""
return self._api_client.go_to_floor(self._elevator_id, floor, immediate)
def set_up_indicator(self, value: bool) -> None:
"""设置上行指示器"""
self._api_client.set_indicators(self._elevator_id, up=value)
def set_down_indicator(self, value: bool) -> None:
"""设置下行指示器"""
self._api_client.set_indicators(self._elevator_id, down=value)
def __setattr__(self, name: str, value: Any) -> None:
"""禁止修改属性,保持只读特性"""
if not self.init_ok:
if not self._init_ok:
object.__setattr__(self, name, value)
else:
raise AttributeError(f"Cannot modify read-only attribute '{name}'")
@@ -128,43 +106,37 @@ class ProxyPassenger(PassengerInfo):
直接使用 PassengerInfo 数据模型实例,提供完整的类型安全访问
"""
init_ok = False
_init_ok = False
def __init__(self, passenger_id: int, api_client: ElevatorAPIClient):
self._passenger_id = passenger_id
self._api_client = api_client
self.init_ok = True
self._init_ok = True
def _get_passenger_info(self) -> PassengerInfo:
"""获取 PassengerInfo 实例"""
# 获取当前状态
state = self._api_client.get_state()
# 在乘客字典中查找
passenger_data = state.passengers.get(self._passenger_id)
if passenger_data is None:
raise AttributeError(f"Passenger {self._passenger_id} not found")
raise ValueError(f"Passenger {self._passenger_id} not found in state")
return passenger_data
# 如果是字典,转换为 PassengerInfo 实例
if isinstance(passenger_data, dict):
return PassengerInfo.from_dict(passenger_data)
def __getattribute__(self, name: str) -> Any:
if not name.startswith("_") and self._init_ok and name not in self.__class__.__dict__:
try:
self_attr = object.__getattribute__(self, name)
if callable(self_attr):
return object.__getattribute__(self, name)
except AttributeError:
pass
psg_info = self._get_passenger_info()
return psg_info.__getattribute__(name)
else:
# 如果已经是 PassengerInfo 实例,直接返回
return passenger_data
def __getattr__(self, name: str) -> Any:
"""动态获取乘客属性"""
try:
passenger_info = self._get_passenger_info()
# 直接从 PassengerInfo 实例获取属性
return getattr(passenger_info, name)
except AttributeError:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
return object.__getattribute__(self, name)
def __setattr__(self, name: str, value: Any) -> None:
"""禁止修改属性,保持只读特性"""
if not self.init_ok:
if not self._init_ok:
object.__setattr__(self, name, value)
else:
raise AttributeError(f"Cannot modify read-only attribute '{name}'")

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
from typing import List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from elevator_saga.core.models import Direction, SimulationEvent
class ElevatorBusExampleController(ElevatorController):
def __init__(self) -> None:
super().__init__("http://127.0.0.1:8000", True)
self.all_passengers: List[ProxyPassenger] = []
self.max_floor = 0
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
self.max_floor = floors[-1].floor
self.floors = floors
for i, elevator in enumerate(elevators):
# 计算目标楼层 - 均匀分布在不同楼层
target_floor = (i * (len(floors) - 1)) // len(elevators)
# 立刻移动到目标位置并开始循环
elevator.go_to_floor(target_floor, immediate=True)
pass
def on_event_execute_start(
self, tick: int, events: List[SimulationEvent], elevators: List[ProxyElevator], floors: List[ProxyFloor]
) -> None:
print(f"Tick {tick}: 即将处理 {len(events)} 个事件 {[e.type.value for e in events]}")
for i in elevators:
print(
f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]"
+ "👦" * len(i.passengers),
end="",
)
print()
def on_event_execute_end(
self, tick: int, events: List[SimulationEvent], elevators: List[ProxyElevator], floors: List[ProxyFloor]
) -> None:
pass
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
self.all_passengers.append(passenger)
print(f"乘客 {passenger.id} F{floor.floor} 请求 {passenger.origin} -> {passenger.destination} ({direction})")
pass
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
elevator.go_to_floor(1)
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
print(f"🛑 电梯 E{elevator.id} 停靠在 F{floor.floor}")
# BUS调度算法电梯到达顶层后立即下降一层
if elevator.last_tick_direction == Direction.UP and elevator.current_floor == self.max_floor:
elevator.go_to_floor(elevator.current_floor - 1)
# 电梯到达底层后,立即上升一层
elif elevator.last_tick_direction == Direction.DOWN and elevator.current_floor == 0:
elevator.go_to_floor(elevator.current_floor + 1)
elif elevator.last_tick_direction == Direction.UP:
elevator.go_to_floor(elevator.current_floor + 1)
elif elevator.last_tick_direction == Direction.DOWN:
elevator.go_to_floor(elevator.current_floor - 1)
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger) -> None:
print(f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}")
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
print(f" 乘客{passenger.id} E{elevator.id}⬇️ F{floor.floor}")
def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
pass
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
pass
def on_elevator_move(
self, elevator: ProxyElevator, from_position: float, to_position: float, direction: str, status: str
) -> None:
pass
if __name__ == "__main__":
algorithm = ElevatorBusExampleController()
algorithm.start()

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
公交车式电梯调度算法示例
电梯像公交车一样运营,按固定路线循环停靠每一层
"""
from typing import Dict, List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from elevator_saga.core.models import Direction, SimulationEvent
class ElevatorBusController(ElevatorController):
"""
公交车式电梯调度算法
电梯像公交车一样按固定路线循环运行,在每层都停
"""
def __init__(self, server_url: str = "http://127.0.0.1:8000", debug: bool = False):
"""初始化控制器"""
super().__init__(server_url, debug)
self.elevator_directions: Dict[int, str] = {} # 记录每个电梯的当前方向
self.max_floor = 0 # 最大楼层数
def on_init(self, elevators: List[ProxyElevator], floors: List[ProxyFloor]) -> None:
"""初始化公交车式电梯算法"""
print("🚌 公交车式电梯算法初始化")
print(f" 管理 {len(elevators)} 部电梯")
print(f" 服务 {len(floors)} 层楼")
# 获取最大楼层数
self.max_floor = len(floors) - 1
# 初始化每个电梯的方向 - 开始都向上
for elevator in elevators:
self.elevator_directions[elevator.id] = "up"
# 简单的初始分布 - 均匀分散到不同楼层
for i, elevator in enumerate(elevators):
# 计算目标楼层 - 均匀分布在不同楼层
target_floor = (i * (len(floors) - 1)) // len(elevators)
# 立刻移动到目标位置并开始循环
elevator.go_to_floor(target_floor, immediate=True)
def on_event_execute_start(
self, tick: int, events: List[SimulationEvent], elevators: List[ProxyElevator], floors: List[ProxyFloor]
) -> None:
"""事件执行前的回调"""
print(f"Tick {tick}: 即将处理 {len(events)} 个事件 {[e.type.value for e in events]}")
for i in elevators:
print(
f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]"
+ "👦" * len(i.passengers),
end="",
)
print()
def on_event_execute_end(
self, tick: int, events: List[SimulationEvent], elevators: List[ProxyElevator], floors: List[ProxyFloor]
) -> None:
"""事件执行后的回调"""
# print(f"✅ Tick {tick}: 已处理 {len(events)} 个事件")
pass
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""
乘客呼叫时的回调
公交车模式下,电梯已经在循环运行,无需特别响应呼叫
"""
print(f"乘客 {passenger.id} F{floor.floor} 请求 {passenger.origin} -> {passenger.destination} ({direction})")
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
"""
电梯空闲时的回调
让空闲的电梯继续执行公交车循环路线,每次移动一层楼
"""
print(f"🛑 电梯 E{elevator.id} 在 F{elevator.current_floor} 层空闲")
# 设置指示器让乘客知道电梯的行进方向
if self.elevator_directions[elevator.id] == "down" and elevator.current_floor != 0:
elevator.go_to_floor(elevator.current_floor - 1, immediate=True)
# elevator.set_up_indicator(True)
elevator.go_to_floor(1)
# current_direction = self.elevator_directions[elevator.id]
# if current_direction == "up":
# elevator.set_up_indicator(True)
# elevator.set_down_indicator(False)
# else:
# elevator.set_up_indicator(False)
# elevator.set_down_indicator(True)
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
"""
电梯停靠时的回调
公交车模式下,在每一层都停下,然后继续下一站
需要注意的是stopped会比idle先触发
"""
print(f"🛑 电梯 E{elevator.id} 停靠在 F{floor.floor}")
if self.elevator_directions[elevator.id] == "up" and elevator.current_floor == self.max_floor:
elevator.go_to_floor(elevator.current_floor - 1, immediate=True)
self.elevator_directions[elevator.id] = "down"
elif self.elevator_directions[elevator.id] == "down" and elevator.current_floor == 0:
elevator.go_to_floor(elevator.current_floor + 1, immediate=True)
self.elevator_directions[elevator.id] = "up"
elif self.elevator_directions[elevator.id] == "up":
# if elevator.id == 0:
# raise ValueError("这里故意要求0号电梯不可能触发非两端停止通过on_elevator_approaching实现")
elevator.go_to_floor(elevator.current_floor + 1, immediate=True)
# 这里故意少写下降的情况用于了解stopped会先于idle触发
# elif self.elevator_directions[elevator.id] == "down":
# elevator.go_to_floor(elevator.current_floor - 1, immediate=True)
# self.elevator_directions[elevator.id] = "down"
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger) -> None:
"""
乘客上梯时的回调
打印乘客上梯信息
"""
print(f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}")
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
"""
乘客下车时的回调
打印乘客下车信息
"""
print(f" 乘客{passenger.id} E{elevator.id}⬇️ F{floor.floor}")
def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
"""
电梯经过楼层时的回调
打印经过楼层的信息
"""
print(f"🔄 电梯 E{elevator.id} 经过 F{floor.floor} (方向: {direction})")
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
"""
电梯即将到达时的回调 (START_DOWN事件)
电梯开始减速,即将到达目标楼层
"""
print(f"🎯 电梯 E{elevator.id} 即将到达 F{floor.floor} (方向: {direction})")
if elevator.target_floor == floor.floor and elevator.target_floor_direction == Direction.UP: # 电梯的目标楼层就是即将停靠的楼层
if elevator.id == 0: # 这里为了测试让0号电梯往上一层就新加一层上行永远不会开门
elevator.go_to_floor(elevator.target_floor + 1, immediate=True)
print(f" 不让0号电梯上行停站设定新目标楼层 {elevator.target_floor + 1}")
def on_elevator_move(
self, elevator: ProxyElevator, from_position: float, to_position: float, direction: str, status: str
) -> None:
"""
电梯移动时的回调
可以在这里记录电梯移动信息,用于调试或性能分析
"""
# 取消注释以显示电梯移动信息
# print(f"🚀 电梯 E{elevator.id} 移动: {from_position:.1f} -> {to_position:.1f} ({direction}, {status})")
pass
if __name__ == "__main__":
algorithm = ElevatorBusController(debug=True)
algorithm.start()

View File

@@ -8,7 +8,7 @@ import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
# 类型变量
T = TypeVar("T", bound="SerializableModel")
@@ -55,6 +55,7 @@ class EventType(Enum):
IDLE = "idle"
PASSENGER_BOARD = "passenger_board"
PASSENGER_ALIGHT = "passenger_alight"
ELEVATOR_MOVE = "elevator_move" # 电梯移动事件
class SerializableModel:
@@ -78,7 +79,12 @@ class SerializableModel:
valid_params = set(sig.parameters.keys()) - {"self"}
filtered_data = {k: v for k, v in data.items() if k in valid_params}
return cls(**filtered_data)
instance = cls(**filtered_data)
for k, v in cls.__dict__.items():
if issubclass(v.__class__, Enum): # 要求不能为None
value = getattr(instance, k)
setattr(instance, k, v.__class__(value))
return instance
@classmethod
def from_json(cls: Type[T], json_str: str) -> T:
@@ -107,10 +113,10 @@ class Position(SerializableModel):
floor_up_position: int = 0
@property
def current_floor_float(self):
return self.current_floor + self.floor_up_position / 10
def current_floor_float(self) -> float:
return round(self.current_floor + self.floor_up_position / 10, 1)
def floor_up_position_add(self, num: int):
def floor_up_position_add(self, num: int) -> int:
self.floor_up_position += num
# 处理向上楼层跨越
@@ -133,7 +139,7 @@ class ElevatorIndicators(SerializableModel):
up: bool = False
down: bool = False
def set_direction(self, direction: Direction):
def set_direction(self, direction: Direction) -> None:
"""根据方向设置指示灯"""
if direction == Direction.UP:
self.up = True
@@ -169,13 +175,13 @@ class PassengerInfo(SerializableModel):
return PassengerStatus.WAITING
@property
def wait_time(self) -> int:
"""等待时间"""
def floor_wait_time(self) -> int:
"""在楼层等待时间(从到达到上电梯)"""
return self.pickup_tick - self.arrive_tick
@property
def system_time(self) -> int:
"""系统时间(总时间"""
def arrival_wait_time(self) -> int:
"""总等待时间(从到达到下电梯"""
return self.dropoff_tick - self.arrive_tick
@property
@@ -196,12 +202,13 @@ class ElevatorState(SerializableModel):
id: int
position: Position
next_target_floor: Optional[int] = None
passengers: List[int] = field(default_factory=list) # type: ignore[reportUnknownVariableType] 乘客ID列表
passengers: List[int] = field(default_factory=list) # 乘客ID列表
max_capacity: int = 10
speed_pre_tick: float = 0.5
run_status: ElevatorStatus = ElevatorStatus.STOPPED
last_tick_direction: Direction = Direction.STOPPED
indicators: ElevatorIndicators = field(default_factory=ElevatorIndicators)
passenger_destinations: Dict[int, int] = field(default_factory=dict) # type: ignore[reportUnknownVariableType] 乘客ID -> 目的地楼层映射
passenger_destinations: Dict[int, int] = field(default_factory=dict) # 乘客ID -> 目的地楼层映射
energy_consumed: float = 0.0
last_update_tick: int = 0
@@ -216,7 +223,7 @@ class ElevatorState(SerializableModel):
def current_floor_float(self) -> float:
"""当前楼层"""
if isinstance(self.position, dict):
self.position = Position.from_dict(self.position)
self.position = Position.from_dict(self.position) # type: ignore[arg-type]
return self.position.current_floor_float
@property
@@ -262,7 +269,7 @@ class ElevatorState(SerializableModel):
"""按下的楼层(基于当前乘客的目的地动态计算)"""
return sorted(list(set(self.passenger_destinations.values())))
def clear_destinations(self):
def clear_destinations(self) -> None:
"""清空目标队列"""
self.next_target_floor = None
@@ -272,8 +279,8 @@ class FloorState(SerializableModel):
"""楼层状态"""
floor: int
up_queue: List[int] = field(default_factory=list) # type: ignore[reportUnknownVariableType] 等待上行的乘客ID
down_queue: List[int] = field(default_factory=list) # type: ignore[reportUnknownVariableType] 等待下行的乘客ID
up_queue: List[int] = field(default_factory=list) # 等待上行的乘客ID
down_queue: List[int] = field(default_factory=list) # 等待下行的乘客ID
@property
def has_waiting_passengers(self) -> bool:
@@ -285,7 +292,7 @@ class FloorState(SerializableModel):
"""总等待人数"""
return len(self.up_queue) + len(self.down_queue)
def add_waiting_passenger(self, passenger_id: int, direction: Direction):
def add_waiting_passenger(self, passenger_id: int, direction: Direction) -> None:
"""添加等待乘客"""
if direction == Direction.UP:
if passenger_id not in self.up_queue:
@@ -314,7 +321,7 @@ class SimulationEvent(SerializableModel):
data: Dict[str, Any]
timestamp: Optional[str] = None
def __post_init__(self):
def __post_init__(self) -> None:
if self.timestamp is None:
self.timestamp = datetime.now().isoformat()
@@ -325,10 +332,10 @@ class PerformanceMetrics(SerializableModel):
completed_passengers: int = 0
total_passengers: int = 0
average_wait_time: float = 0.0
p95_wait_time: float = 0.0
average_system_time: float = 0.0
p95_system_time: float = 0.0
average_floor_wait_time: float = 0.0
p95_floor_wait_time: float = 0.0
average_arrival_wait_time: float = 0.0
p95_arrival_wait_time: float = 0.0
# total_energy_consumption: float = 0.0
@property
@@ -353,9 +360,9 @@ class SimulationState(SerializableModel):
tick: int
elevators: List[ElevatorState]
floors: List[FloorState]
passengers: Dict[int, PassengerInfo] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
passengers: Dict[int, PassengerInfo] = field(default_factory=dict)
metrics: PerformanceMetrics = field(default_factory=PerformanceMetrics)
events: List[SimulationEvent] = field(default_factory=list) # type: ignore[reportUnknownVariableType]
events: List[SimulationEvent] = field(default_factory=list)
def get_elevator_by_id(self, elevator_id: int) -> Optional[ElevatorState]:
"""根据ID获取电梯"""
@@ -375,7 +382,7 @@ class SimulationState(SerializableModel):
"""根据状态获取乘客"""
return [p for p in self.passengers.values() if p.status == status]
def add_event(self, event_type: EventType, data: Dict[str, Any]):
def add_event(self, event_type: EventType, data: Dict[str, Any]) -> None:
"""添加事件"""
event = SimulationEvent(tick=self.tick, type=event_type, data=data)
self.events.append(event)
@@ -415,7 +422,7 @@ class StepResponse(SerializableModel):
success: bool
tick: int
events: List[SimulationEvent] = field(default_factory=list) # type: ignore[reportUnknownVariableType]
events: List[SimulationEvent] = field(default_factory=list)
request_id: Optional[str] = None
error_message: Optional[str] = None
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
@@ -435,8 +442,8 @@ class ElevatorCommand(SerializableModel):
"""电梯命令"""
elevator_id: int
command_type: str # "go_to_floor", "stop", "set_indicators"
parameters: Dict[str, Any] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
command_type: str # "go_to_floor", "stop"
parameters: Dict[str, Any] = field(default_factory=dict)
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
@@ -465,22 +472,6 @@ class GoToFloorCommand(SerializableModel):
return {"floor": self.floor, "immediate": self.immediate}
@dataclass
class SetIndicatorsCommand(SerializableModel):
"""设置指示灯命令"""
elevator_id: int
up: Optional[bool] = None
down: Optional[bool] = None
command_type: str = field(default="set_indicators", init=False)
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
@property
def parameters(self) -> Dict[str, Any]:
return {"up": self.up, "down": self.down}
# ==================== 流量和配置数据模型 ====================
@@ -503,7 +494,7 @@ class TrafficPattern(SerializableModel):
entries: List[TrafficEntry] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def add_entry(self, entry: TrafficEntry):
def add_entry(self, entry: TrafficEntry) -> None:
"""添加流量条目"""
self.entries.append(entry)

View File

@@ -7,7 +7,7 @@ from typing import Dict, List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from elevator_saga.core.models import SimulationEvent, Direction
from elevator_saga.core.models import Direction, SimulationEvent
class ElevatorBusController(ElevatorController):
@@ -45,17 +45,20 @@ class ElevatorBusController(ElevatorController):
"""事件执行前的回调"""
print(f"Tick {tick}: 即将处理 {len(events)} 个事件 {[e.type.value for e in events]}")
for i in elevators:
print(f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]" + "👦" * len(i.passengers), end="")
print(
f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]"
+ "👦" * len(i.passengers),
end="",
)
print()
def on_event_execute_end(
self, tick: int, events: List[SimulationEvent], elevators: List[ProxyElevator], floors: List[ProxyFloor]
) -> None:
"""事件执行后的回调"""
# print(f"✅ Tick {tick}: 已处理 {len(events)} 个事件")
pass
def on_passenger_call(self, passenger:ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""
乘客呼叫时的回调
公交车模式下,电梯已经在循环运行,无需特别响应呼叫
@@ -108,9 +111,7 @@ class ElevatorBusController(ElevatorController):
乘客上梯时的回调
打印乘客上梯信息
"""
print(
f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}"
)
print(f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}")
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
"""
@@ -137,6 +138,7 @@ class ElevatorBusController(ElevatorController):
elevator.go_to_floor(elevator.target_floor + 1, immediate=True)
print(f" 不让0号电梯上行停站设定新目标楼层 {elevator.target_floor + 1}")
if __name__ == "__main__":
algorithm = ElevatorBusController(debug=True)
algorithm.start()

View File

@@ -10,7 +10,7 @@ import threading
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, cast
from typing import Any, Dict, List, cast
from flask import Flask, Response, request
@@ -22,6 +22,7 @@ from elevator_saga.core.models import (
FloorState,
PassengerInfo,
PassengerStatus,
PerformanceMetrics,
SerializableModel,
SimulationEvent,
SimulationState,
@@ -33,13 +34,13 @@ from elevator_saga.core.models import (
_SERVER_DEBUG_MODE = False
def set_server_debug_mode(enabled: bool):
def set_server_debug_mode(enabled: bool) -> None:
"""Enable or disable server debug logging"""
global _SERVER_DEBUG_MODE
globals()["_SERVER_DEBUG_MODE"] = enabled
def server_debug_log(message: str):
def server_debug_log(message: str) -> None:
"""Print server debug message if debug mode is enabled"""
if _SERVER_DEBUG_MODE:
print(f"[SERVER-DEBUG] {message}", flush=True)
@@ -89,19 +90,6 @@ def json_response(data: Any, status: int = 200) -> Response | tuple[Response, in
return response, status
@dataclass
class MetricsResponse(SerializableModel):
"""性能指标响应"""
done: int
total: int
avg_wait: float
p95_wait: float
avg_system: float
p95_system: float
energy_total: float
@dataclass
class PassengerSummary(SerializableModel):
"""乘客摘要"""
@@ -120,7 +108,7 @@ class SimulationStateResponse(SerializableModel):
elevators: List[ElevatorState]
floors: List[FloorState]
passengers: Dict[int, PassengerInfo]
metrics: MetricsResponse
metrics: PerformanceMetrics
class ElevatorSimulation:
@@ -273,21 +261,82 @@ class ElevatorSimulation:
return new_events
def _process_tick(self) -> List[SimulationEvent]:
"""Process one simulation tick"""
"""
Process one simulation tick
每个tick先发生事件再发生动作
"""
events_start = len(self.state.events)
self._update_elevator_status()
# 1. Add new passengers from traffic queue
self._process_arrivals()
# 2. Move elevators
self._move_elevators()
# 3. Process elevator stops and passenger boarding/alighting
# 3. Process elevator stops and passenger alighting
self._process_elevator_stops()
# Return events generated this tick
return self.state.events[events_start:]
def _process_arrivals(self) -> None:
def _process_passenger_in(self, elevator: ElevatorState) -> None:
current_floor = elevator.current_floor
# 处于Stopped状态方向也已经清空说明没有调度。
floor = self.floors[current_floor]
passengers_to_board: List[int] = []
available_capacity = elevator.max_capacity - len(elevator.passengers)
# Board passengers going up (if up indicator is on or no direction set)
if elevator.target_floor_direction == Direction.UP:
passengers_to_board.extend(floor.up_queue[:available_capacity])
floor.up_queue = floor.up_queue[available_capacity:]
# Board passengers going down (if down indicator is on or no direction set)
if elevator.target_floor_direction == Direction.DOWN:
passengers_to_board.extend(floor.down_queue[:available_capacity])
floor.down_queue = floor.down_queue[available_capacity:]
# Process boarding
for passenger_id in passengers_to_board:
passenger = self.passengers[passenger_id]
passenger.pickup_tick = self.tick
passenger.elevator_id = elevator.id
elevator.passengers.append(passenger_id)
self._emit_event(
EventType.PASSENGER_BOARD,
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id},
)
def _update_elevator_status(self) -> None:
"""更新电梯运行状态"""
for elevator in self.elevators:
target_floor = elevator.target_floor
old_status = elevator.run_status.value
# 没有移动方向,说明电梯已经到达目标楼层
if elevator.target_floor_direction == Direction.STOPPED:
if elevator.next_target_floor is not None:
self._set_elevator_target_floor(elevator, elevator.next_target_floor)
self._process_passenger_in(elevator)
elevator.next_target_floor = None
else:
continue
# 有移动方向,但是需要启动了
if elevator.run_status == ElevatorStatus.STOPPED:
# 从停止状态启动 - 注意START_UP表示启动加速状态不表示方向
# 实际移动方向由target_floor_direction决定
elevator.run_status = ElevatorStatus.START_UP
# 从启动状态切换到匀速
elif elevator.run_status == ElevatorStatus.START_UP:
# 从启动状态切换到匀速
elevator.run_status = ElevatorStatus.CONSTANT_SPEED
server_debug_log(
f"电梯{elevator.id} 状态:{old_status}->{elevator.run_status.value} 方向:{elevator.target_floor_direction.value} "
f"位置:{elevator.position.current_floor_float:.1f} 目标:{target_floor}"
)
# START_DOWN状态会在到达目标时在_move_elevators中切换为STOPPED
def _process_arrivals(self) -> None: # OK
"""Process new passenger arrivals"""
while self.traffic_queue and self.traffic_queue[0].tick <= self.tick:
traffic_entry = self.traffic_queue.pop(0)
@@ -297,9 +346,7 @@ class ElevatorSimulation:
destination=traffic_entry.destination,
arrive_tick=self.tick,
)
assert (
traffic_entry.origin != traffic_entry.destination
), f"乘客{passenger.id}目的地和起始地{traffic_entry.origin}重复"
assert traffic_entry.origin != traffic_entry.destination, f"乘客{passenger.id}目的地和起始地{traffic_entry.origin}重复"
self.passengers[passenger.id] = passenger
server_debug_log(f"乘客 {passenger.id:4} 创建 | {passenger}")
if passenger.destination > passenger.origin:
@@ -309,6 +356,143 @@ class ElevatorSimulation:
self.floors[passenger.origin].down_queue.append(passenger.id)
self._emit_event(EventType.DOWN_BUTTON_PRESSED, {"floor": passenger.origin, "passenger": passenger.id})
def _move_elevators(self) -> None:
"""
Move all elevators towards their destinations with acceleration/deceleration
上一步已经处理了当前电梯的状态,这里只做移动
"""
for elevator in self.elevators:
target_floor = elevator.target_floor
new_floor = old_floor = elevator.position.current_floor
# 获取移动速度
movement_speed = 0
if elevator.run_status == ElevatorStatus.START_UP:
movement_speed = 1
elif elevator.run_status == ElevatorStatus.START_DOWN:
movement_speed = 1
elif elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
movement_speed = 2
if movement_speed == 0:
continue
# 根据状态和方向调整移动距离
elevator.last_tick_direction = elevator.target_floor_direction
old_position = elevator.position.current_floor_float
if elevator.target_floor_direction == Direction.UP:
new_floor = elevator.position.floor_up_position_add(movement_speed)
elif elevator.target_floor_direction == Direction.DOWN:
new_floor = elevator.position.floor_up_position_add(-movement_speed)
else:
# 之前的状态已经是到站了,清空上一次到站的方向
pass
# 发送电梯移动事件
if elevator.target_floor_direction != Direction.STOPPED:
self._emit_event(
EventType.ELEVATOR_MOVE,
{
"elevator": elevator.id,
"from_position": old_position,
"to_position": elevator.position.current_floor_float,
"direction": elevator.target_floor_direction.value,
"status": elevator.run_status.value,
},
)
# 移动后检测是否即将到站,从匀速状态切换到减速
if elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
# 检查是否需要开始减速这里加速减速设置路程为1匀速路程为2这样能够保证不会匀速恰好到达必须加减速
# 如果速度超出,则预期的逻辑是,恰好到达/超出0等会强制触发start_down多走一次才能stop目前没有实现这部分逻辑
if self._should_start_deceleration(elevator):
elevator.run_status = ElevatorStatus.START_DOWN
# 发送电梯即将经过某层楼事件
if self._near_next_stop(elevator):
self._emit_event(
EventType.ELEVATOR_APPROACHING,
{
"elevator": elevator.id,
"floor": int(round(elevator.position.current_floor_float)),
"direction": elevator.target_floor_direction.value,
},
)
# 处理楼层变化事件
if old_floor != new_floor:
if new_floor != target_floor:
self._emit_event(
EventType.PASSING_FLOOR,
{
"elevator": elevator.id,
"floor": new_floor,
"direction": elevator.target_floor_direction.value,
},
)
# 检查是否到达目标楼层
if target_floor == new_floor and elevator.position.floor_up_position == 0:
elevator.run_status = ElevatorStatus.STOPPED
# 刚进入Stopped状态可以通过last_direction识别
self._emit_event(
EventType.STOPPED_AT_FLOOR, {"elevator": elevator.id, "floor": new_floor, "reason": "move_reached"}
)
# elevator.energy_consumed += abs(direction * elevator.speed_pre_tick) * 0.5
def _process_elevator_stops(self) -> None:
"""
处理Stopped电梯上下客新target处理等。
"""
for elevator in self.elevators:
current_floor = elevator.current_floor
# 处于Stopped状态方向也已经清空说明没有调度。
if elevator.last_tick_direction == Direction.STOPPED:
self._emit_event(EventType.IDLE, {"elevator": elevator.id, "floor": current_floor})
continue
# 其他处于STOPPED状态刚进入stop到站要进行上下客
if not elevator.run_status == ElevatorStatus.STOPPED:
continue
# Let passengers alight
passengers_to_remove: List[int] = []
for passenger_id in elevator.passengers:
passenger = self.passengers[passenger_id]
if passenger.destination == current_floor:
passenger.dropoff_tick = self.tick
passengers_to_remove.append(passenger_id)
# Remove passengers who alighted
for passenger_id in passengers_to_remove:
elevator.passengers.remove(passenger_id)
self._emit_event(
EventType.PASSENGER_ALIGHT,
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id},
)
# Board waiting passengers (if indicators allow)
if elevator.next_target_floor is not None:
self._set_elevator_target_floor(elevator, elevator.next_target_floor)
elevator.next_target_floor = None
def _set_elevator_target_floor(self, elevator: ElevatorState, floor: int) -> None:
"""
同一个tick内提示
[SERVER-DEBUG] 电梯 E0 下一目的地设定为 F1
[SERVER-DEBUG] 电梯 E0 被设定为前往 F1
说明电梯处于stop状态这个tick直接采用下一个目的地运行了
"""
elevator.position.target_floor = floor
server_debug_log(f"电梯 E{elevator.id} 被设定为前往 F{floor}")
new_target_floor_should_accel = self._should_start_deceleration(elevator)
if not new_target_floor_should_accel:
if elevator.run_status == ElevatorStatus.START_DOWN: # 不应该加速但是加了
elevator.run_status = ElevatorStatus.CONSTANT_SPEED
server_debug_log(f"电梯 E{elevator.id} 被设定为匀速")
elif new_target_floor_should_accel:
if elevator.run_status == ElevatorStatus.CONSTANT_SPEED: # 应该减速了,但是之前是匀速
elevator.run_status = ElevatorStatus.START_DOWN
server_debug_log(f"电梯 E{elevator.id} 被设定为减速")
if elevator.current_floor != floor or elevator.position.floor_up_position != 0:
old_status = elevator.run_status.value
server_debug_log(f"电梯{elevator.id} 状态:{old_status}->{elevator.run_status.value}")
def _calculate_distance_to_target(self, elevator: ElevatorState) -> float:
"""计算到目标楼层的距离以floor_up_position为单位"""
current_pos = elevator.position.current_floor * 10 + elevator.position.floor_up_position
@@ -336,214 +520,18 @@ class ElevatorSimulation:
distance = self._calculate_distance_to_near_stop(elevator)
return distance == 1
def _get_movement_speed(self, elevator: ElevatorState) -> int:
"""根据电梯状态获取移动速度"""
if elevator.run_status == ElevatorStatus.START_UP:
return 1
elif elevator.run_status == ElevatorStatus.START_DOWN:
return 1
elif elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
return 2
else: # STOPPED
return 0
def _update_elevator_status(self, elevator: ElevatorState) -> None:
"""更新电梯运行状态"""
current_floor = elevator.position.current_floor
target_floor = elevator.target_floor
if current_floor == target_floor and elevator.position.floor_up_position == 0:
# 已到达目标楼层
elevator.run_status = ElevatorStatus.STOPPED
return
if elevator.run_status == ElevatorStatus.STOPPED:
# 从停止状态启动 - 注意START_UP表示启动加速状态不表示方向
# 实际移动方向由target_floor_direction决定
elevator.run_status = ElevatorStatus.START_UP
elif elevator.run_status == ElevatorStatus.START_UP:
# 从启动状态切换到匀速
elevator.run_status = ElevatorStatus.CONSTANT_SPEED
elif elevator.run_status == ElevatorStatus.CONSTANT_SPEED:
# 检查是否需要开始减速这里加速减速设置路程为1匀速路程为2这样能够保证不会匀速恰好到达必须加减速
# 如果速度超出,则预期的逻辑是,恰好到达/超出0等会强制触发start_down多走一次才能stop目前没有实现这部分逻辑
if self._should_start_deceleration(elevator):
elevator.run_status = ElevatorStatus.START_DOWN
# 发送电梯即将经过某层楼事件
if self._near_next_stop(elevator):
self._emit_event(
EventType.ELEVATOR_APPROACHING,
{
"elevator": elevator.id,
"floor": elevator.target_floor,
"direction": elevator.target_floor_direction.value,
},
)
# START_DOWN状态会在到达目标时自动切换为STOPPED
def _move_elevators(self) -> None:
"""Move all elevators towards their destinations with acceleration/deceleration"""
for elevator in self.elevators:
target_floor = elevator.target_floor
current_floor = elevator.position.current_floor
# 如果已在恰好目标楼层标记为STOPPED之后交给_process_elevator_stops处理
if target_floor == current_floor:
if elevator.next_target_floor is None:
continue
if elevator.position.floor_up_position == 0:
server_debug_log(
f"电梯{elevator.id}已在目标楼层,当前{elevator.position.current_floor_float} / 目标{target_floor}"
)
elevator.run_status = ElevatorStatus.STOPPED
continue
# 获取移动速度
movement_speed = self._get_movement_speed(elevator)
if movement_speed == 0:
continue
# Move towards target
old_floor = current_floor
# 根据状态和方向调整移动速度
if elevator.target_floor_direction == Direction.UP:
# 向上移动
new_floor = elevator.position.floor_up_position_add(movement_speed)
else:
# 向下移动
new_floor = elevator.position.floor_up_position_add(-movement_speed)
# 更新电梯状态
old_status = elevator.run_status.value
self._update_elevator_status(elevator)
server_debug_log(
f"电梯{elevator.id} 状态:{old_status}->{elevator.run_status.value} 方向:{elevator.target_floor_direction.value} 速度:{movement_speed} "
f"位置:{elevator.position.current_floor_float:.1f} 目标:{target_floor}"
)
# 处理楼层变化事件
if old_floor != new_floor:
if new_floor != target_floor:
self._emit_event(
EventType.PASSING_FLOOR,
{
"elevator": elevator.id,
"floor": new_floor,
"direction": elevator.target_floor_direction.value,
},
)
# 检查是否到达目标楼层
if target_floor == new_floor and elevator.position.floor_up_position == 0:
elevator.run_status = ElevatorStatus.STOPPED
self._emit_event(EventType.STOPPED_AT_FLOOR, {"elevator": elevator.id, "floor": new_floor})
elevator.indicators.set_direction(elevator.target_floor_direction) # 抵达目标楼层说明该取消floor了
# elevator.energy_consumed += abs(direction * elevator.speed_pre_tick) * 0.5
def _process_elevator_stops(self) -> None:
"""Handle passenger boarding and alighting at elevator stops"""
for elevator in self.elevators:
if not elevator.run_status == ElevatorStatus.STOPPED:
continue
current_floor = elevator.current_floor
# Let passengers alight
passengers_to_remove: List[int] = []
for passenger_id in elevator.passengers:
passenger = self.passengers[passenger_id]
if passenger.destination == current_floor:
passenger.dropoff_tick = self.tick
passengers_to_remove.append(passenger_id)
# Remove passengers who alighted
for passenger_id in passengers_to_remove:
elevator.passengers.remove(passenger_id)
self._emit_event(
EventType.PASSENGER_ALIGHT,
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id},
)
# Board waiting passengers (if indicators allow)
floor = self.floors[current_floor]
passengers_to_board: List[int] = []
if not elevator.indicators.up and not elevator.indicators.down:
if elevator.next_target_floor is not None:
self._set_elevator_target_floor(elevator, elevator.next_target_floor)
elevator.next_target_floor = None
elevator.indicators.set_direction(elevator.target_floor_direction)
# Board passengers going up (if up indicator is on or no direction set)
if elevator.indicators.up:
available_capacity = elevator.max_capacity - len(elevator.passengers)
passengers_to_board.extend(floor.up_queue[:available_capacity])
floor.up_queue = floor.up_queue[available_capacity:]
# Board passengers going down (if down indicator is on or no direction set)
if elevator.indicators.down:
# 先临时计算长度
remaining_capacity = elevator.max_capacity - len(elevator.passengers) - len(passengers_to_board)
if remaining_capacity > 0:
down_passengers = floor.down_queue[:remaining_capacity]
passengers_to_board.extend(down_passengers)
floor.down_queue = floor.down_queue[remaining_capacity:]
# 没有上下指示的时候触发等待会消耗一个tick
if not elevator.indicators.up and not elevator.indicators.down:
self._emit_event(EventType.IDLE, {"elevator": elevator.id, "floor": current_floor})
continue
# Process boarding
for passenger_id in passengers_to_board:
passenger = self.passengers[passenger_id]
passenger.pickup_tick = self.tick
passenger.elevator_id = elevator.id
elevator.passengers.append(passenger_id)
self._emit_event(
EventType.PASSENGER_BOARD,
{"elevator": elevator.id, "floor": current_floor, "passenger": passenger_id},
)
def _set_elevator_target_floor(self, elevator: ElevatorState, floor: int):
original_target_floor = elevator.target_floor
elevator.position.target_floor = floor
server_debug_log(f"电梯 E{elevator.id} 被设定为立刻前往 F{floor}")
new_target_floor_should_accel = self._should_start_deceleration(elevator)
if not new_target_floor_should_accel:
if elevator.run_status == ElevatorStatus.START_DOWN: # 不应该加速但是加了
elevator.run_status = ElevatorStatus.CONSTANT_SPEED
server_debug_log(f"电梯 E{elevator.id} 被设定为匀速")
elif new_target_floor_should_accel:
if elevator.run_status == ElevatorStatus.CONSTANT_SPEED: # 应该减速了,但是之前是匀速
elevator.run_status = ElevatorStatus.START_DOWN
server_debug_log(f"电梯 E{elevator.id} 被设定为减速")
if elevator.current_floor != floor or elevator.position.floor_up_position != 0:
old_status = elevator.run_status.value
self._update_elevator_status(elevator)
server_debug_log(
f"电梯{elevator.id} 状态:{old_status}->{elevator.run_status.value} 方向:{elevator.target_floor_direction.value} "
f"位置:{elevator.position.current_floor_float:.1f} 目标:{floor}"
)
def elevator_go_to_floor(self, elevator_id: int, floor: int, immediate: bool = False) -> None:
"""Command elevator to go to specified floor"""
"""
设置电梯去向,是生命周期开始,分配目的地
"""
if 0 <= elevator_id < len(self.elevators) and 0 <= floor < len(self.floors):
elevator = self.elevators[elevator_id]
if immediate:
self._set_elevator_target_floor(elevator, floor)
elevator.indicators.set_direction(elevator.target_floor_direction)
else:
elevator.next_target_floor = floor
server_debug_log(f"电梯 E{elevator_id} 下一目的地设定为 F{floor}")
def elevator_set_indicators(self, elevator_id: int, up: Optional[bool] = None, down: Optional[bool] = None) -> None:
"""Set elevator direction indicators"""
if 0 <= elevator_id < len(self.elevators):
elevator = self.elevators[elevator_id]
if up is not None:
elevator.indicators.up = up
if down is not None:
elevator.indicators.down = down
def get_state(self) -> SimulationStateResponse:
"""Get complete simulation state"""
with self.lock:
@@ -558,41 +546,45 @@ class ElevatorSimulation:
metrics=metrics,
)
def _calculate_metrics(self) -> MetricsResponse:
def _calculate_metrics(self) -> PerformanceMetrics:
"""Calculate performance metrics"""
# 直接从state中筛选已完成的乘客
completed = [p for p in self.state.passengers.values() if p.status == PassengerStatus.COMPLETED]
total_passengers = len(self.state.passengers)
if not completed:
return MetricsResponse(
done=0,
total=total_passengers,
avg_wait=0,
p95_wait=0,
avg_system=0,
p95_system=0,
energy_total=sum(e.energy_consumed for e in self.elevators),
return PerformanceMetrics(
completed_passengers=0,
total_passengers=total_passengers,
average_floor_wait_time=0,
p95_floor_wait_time=0,
average_arrival_wait_time=0,
p95_arrival_wait_time=0,
)
wait_times = [float(p.wait_time) for p in completed]
system_times = [float(p.system_time) for p in completed]
floor_wait_times = [float(p.floor_wait_time) for p in completed]
arrival_wait_times = [float(p.arrival_wait_time) for p in completed]
def percentile(data: List[float], p: int) -> float:
def average_excluding_top_percent(data: List[float], exclude_percent: int) -> float:
"""计算排除掉最长的指定百分比后的平均值"""
if not data:
return 0.0
sorted_data = sorted(data)
index = int(len(sorted_data) * p / 100)
return sorted_data[min(index, len(sorted_data) - 1)]
# 计算要保留的数据数量(排除掉最长的 exclude_percent
keep_count = int(len(sorted_data) * (100 - exclude_percent) / 100)
if keep_count == 0:
return 0.0
# 只保留前 keep_count 个数据,排除最长的部分
kept_data = sorted_data[:keep_count]
return sum(kept_data) / len(kept_data)
return MetricsResponse(
done=len(completed),
total=total_passengers,
avg_wait=sum(wait_times) / len(wait_times) if wait_times else 0,
p95_wait=percentile(wait_times, 95),
avg_system=sum(system_times) / len(system_times) if system_times else 0,
p95_system=percentile(system_times, 95),
energy_total=sum(e.energy_consumed for e in self.elevators),
return PerformanceMetrics(
completed_passengers=len(completed),
total_passengers=total_passengers,
average_floor_wait_time=sum(floor_wait_times) / len(floor_wait_times) if floor_wait_times else 0,
p95_floor_wait_time=average_excluding_top_percent(floor_wait_times, 5),
average_arrival_wait_time=sum(arrival_wait_times) / len(arrival_wait_times) if arrival_wait_times else 0,
p95_arrival_wait_time=average_excluding_top_percent(arrival_wait_times, 5),
)
def get_events(self, since_tick: int = 0) -> List[SimulationEvent]:
@@ -658,8 +650,8 @@ def step_simulation() -> Response | tuple[Response, int]:
try:
data: Dict[str, Any] = request.get_json() or {}
ticks = data.get("ticks", 1)
server_debug_log("")
server_debug_log(f"HTTP /api/step request ----- ticks: {ticks}")
# server_debug_log("")
# server_debug_log(f"HTTP /api/step request ----- ticks: {ticks}")
events = simulation.step(ticks)
server_debug_log(f"HTTP /api/step response ----- tick: {simulation.tick}, events: {len(events)}\n")
return json_response(
@@ -693,18 +685,6 @@ def elevator_go_to_floor(elevator_id: int) -> Response | tuple[Response, int]:
return json_response({"error": str(e)}, 500)
@app.route("/api/elevators/<int:elevator_id>/set_indicators", methods=["POST"])
def elevator_set_indicators(elevator_id: int) -> Response | tuple[Response, int]:
try:
data: Dict[str, Any] = request.get_json() or {}
up = data.get("up")
down = data.get("down")
simulation.elevator_set_indicators(elevator_id, up, down)
return json_response({"success": True})
except Exception as e:
return json_response({"error": str(e)}, 500)
@app.route("/api/traffic/next", methods=["POST"])
def next_traffic_round() -> Response | tuple[Response, int]:
"""切换到下一个流量文件"""

View File

@@ -0,0 +1,458 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "down_peak",
"scale": "medium",
"description": "下行高峰 - 主要从高层到底层 (medium规模)",
"expected_passengers": 74,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 2,
"destination": 0,
"tick": 0
},
{
"id": 2,
"origin": 2,
"destination": 1,
"tick": 3
},
{
"id": 3,
"origin": 4,
"destination": 0,
"tick": 5
},
{
"id": 4,
"origin": 3,
"destination": 0,
"tick": 6
},
{
"id": 5,
"origin": 5,
"destination": 0,
"tick": 7
},
{
"id": 6,
"origin": 4,
"destination": 0,
"tick": 9
},
{
"id": 7,
"origin": 1,
"destination": 0,
"tick": 10
},
{
"id": 8,
"origin": 5,
"destination": 0,
"tick": 12
},
{
"id": 9,
"origin": 3,
"destination": 0,
"tick": 13
},
{
"id": 10,
"origin": 3,
"destination": 0,
"tick": 14
},
{
"id": 11,
"origin": 2,
"destination": 0,
"tick": 15
},
{
"id": 12,
"origin": 1,
"destination": 0,
"tick": 17
},
{
"id": 13,
"origin": 3,
"destination": 2,
"tick": 18
},
{
"id": 14,
"origin": 4,
"destination": 0,
"tick": 19
},
{
"id": 15,
"origin": 3,
"destination": 0,
"tick": 22
},
{
"id": 16,
"origin": 5,
"destination": 4,
"tick": 28
},
{
"id": 17,
"origin": 1,
"destination": 0,
"tick": 30
},
{
"id": 18,
"origin": 3,
"destination": 0,
"tick": 32
},
{
"id": 19,
"origin": 4,
"destination": 0,
"tick": 35
},
{
"id": 20,
"origin": 4,
"destination": 0,
"tick": 37
},
{
"id": 21,
"origin": 1,
"destination": 0,
"tick": 39
},
{
"id": 22,
"origin": 5,
"destination": 0,
"tick": 40
},
{
"id": 23,
"origin": 5,
"destination": 0,
"tick": 41
},
{
"id": 24,
"origin": 5,
"destination": 0,
"tick": 42
},
{
"id": 25,
"origin": 2,
"destination": 1,
"tick": 44
},
{
"id": 26,
"origin": 4,
"destination": 0,
"tick": 45
},
{
"id": 27,
"origin": 1,
"destination": 0,
"tick": 46
},
{
"id": 28,
"origin": 3,
"destination": 0,
"tick": 49
},
{
"id": 29,
"origin": 5,
"destination": 0,
"tick": 50
},
{
"id": 30,
"origin": 5,
"destination": 0,
"tick": 51
},
{
"id": 31,
"origin": 4,
"destination": 3,
"tick": 52
},
{
"id": 32,
"origin": 3,
"destination": 1,
"tick": 53
},
{
"id": 33,
"origin": 3,
"destination": 0,
"tick": 55
},
{
"id": 34,
"origin": 5,
"destination": 0,
"tick": 58
},
{
"id": 35,
"origin": 3,
"destination": 0,
"tick": 59
},
{
"id": 36,
"origin": 4,
"destination": 3,
"tick": 60
},
{
"id": 37,
"origin": 5,
"destination": 0,
"tick": 62
},
{
"id": 38,
"origin": 4,
"destination": 0,
"tick": 63
},
{
"id": 39,
"origin": 4,
"destination": 0,
"tick": 65
},
{
"id": 40,
"origin": 2,
"destination": 0,
"tick": 71
},
{
"id": 41,
"origin": 4,
"destination": 0,
"tick": 73
},
{
"id": 42,
"origin": 4,
"destination": 0,
"tick": 74
},
{
"id": 43,
"origin": 3,
"destination": 0,
"tick": 79
},
{
"id": 44,
"origin": 5,
"destination": 1,
"tick": 83
},
{
"id": 45,
"origin": 3,
"destination": 0,
"tick": 84
},
{
"id": 46,
"origin": 1,
"destination": 0,
"tick": 91
},
{
"id": 47,
"origin": 3,
"destination": 0,
"tick": 94
},
{
"id": 48,
"origin": 2,
"destination": 0,
"tick": 97
},
{
"id": 49,
"origin": 3,
"destination": 0,
"tick": 99
},
{
"id": 50,
"origin": 3,
"destination": 0,
"tick": 104
},
{
"id": 51,
"origin": 5,
"destination": 0,
"tick": 105
},
{
"id": 52,
"origin": 5,
"destination": 0,
"tick": 111
},
{
"id": 53,
"origin": 1,
"destination": 0,
"tick": 115
},
{
"id": 54,
"origin": 2,
"destination": 0,
"tick": 116
},
{
"id": 55,
"origin": 5,
"destination": 0,
"tick": 119
},
{
"id": 56,
"origin": 5,
"destination": 0,
"tick": 122
},
{
"id": 57,
"origin": 3,
"destination": 2,
"tick": 124
},
{
"id": 58,
"origin": 3,
"destination": 2,
"tick": 127
},
{
"id": 59,
"origin": 2,
"destination": 0,
"tick": 129
},
{
"id": 60,
"origin": 3,
"destination": 0,
"tick": 130
},
{
"id": 61,
"origin": 5,
"destination": 0,
"tick": 136
},
{
"id": 62,
"origin": 5,
"destination": 0,
"tick": 138
},
{
"id": 63,
"origin": 2,
"destination": 0,
"tick": 148
},
{
"id": 64,
"origin": 2,
"destination": 0,
"tick": 150
},
{
"id": 65,
"origin": 5,
"destination": 0,
"tick": 153
},
{
"id": 66,
"origin": 3,
"destination": 2,
"tick": 158
},
{
"id": 67,
"origin": 5,
"destination": 0,
"tick": 165
},
{
"id": 68,
"origin": 3,
"destination": 0,
"tick": 166
},
{
"id": 69,
"origin": 2,
"destination": 0,
"tick": 168
},
{
"id": 70,
"origin": 5,
"destination": 0,
"tick": 171
},
{
"id": 71,
"origin": 3,
"destination": 0,
"tick": 172
},
{
"id": 72,
"origin": 4,
"destination": 0,
"tick": 179
},
{
"id": 73,
"origin": 5,
"destination": 0,
"tick": 183
},
{
"id": 74,
"origin": 1,
"destination": 0,
"tick": 196
}
]
}

View File

@@ -0,0 +1,182 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "fire_evacuation",
"scale": "medium",
"description": "火警疏散 - 紧急疏散到大厅 (medium规模)",
"expected_passengers": 28,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 3,
"destination": 5,
"tick": 10
},
{
"id": 2,
"origin": 5,
"destination": 1,
"tick": 14
},
{
"id": 3,
"origin": 0,
"destination": 1,
"tick": 22
},
{
"id": 4,
"origin": 4,
"destination": 2,
"tick": 29
},
{
"id": 5,
"origin": 2,
"destination": 1,
"tick": 30
},
{
"id": 6,
"origin": 5,
"destination": 2,
"tick": 32
},
{
"id": 7,
"origin": 2,
"destination": 5,
"tick": 41
},
{
"id": 8,
"origin": 0,
"destination": 1,
"tick": 51
},
{
"id": 9,
"origin": 1,
"destination": 0,
"tick": 68
},
{
"id": 10,
"origin": 1,
"destination": 0,
"tick": 71
},
{
"id": 11,
"origin": 1,
"destination": 0,
"tick": 73
},
{
"id": 12,
"origin": 1,
"destination": 0,
"tick": 68
},
{
"id": 13,
"origin": 1,
"destination": 0,
"tick": 73
},
{
"id": 14,
"origin": 2,
"destination": 0,
"tick": 76
},
{
"id": 15,
"origin": 2,
"destination": 0,
"tick": 69
},
{
"id": 16,
"origin": 2,
"destination": 0,
"tick": 72
},
{
"id": 17,
"origin": 2,
"destination": 0,
"tick": 76
},
{
"id": 18,
"origin": 3,
"destination": 0,
"tick": 76
},
{
"id": 19,
"origin": 3,
"destination": 0,
"tick": 75
},
{
"id": 20,
"origin": 3,
"destination": 0,
"tick": 66
},
{
"id": 21,
"origin": 4,
"destination": 0,
"tick": 66
},
{
"id": 22,
"origin": 4,
"destination": 0,
"tick": 75
},
{
"id": 23,
"origin": 4,
"destination": 0,
"tick": 72
},
{
"id": 24,
"origin": 4,
"destination": 0,
"tick": 74
},
{
"id": 25,
"origin": 5,
"destination": 0,
"tick": 74
},
{
"id": 26,
"origin": 5,
"destination": 0,
"tick": 70
},
{
"id": 27,
"origin": 5,
"destination": 0,
"tick": 72
},
{
"id": 28,
"origin": 5,
"destination": 0,
"tick": 74
}
]
}

View File

@@ -9,7 +9,7 @@ import math
import os.path
import random
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional
# 建筑规模配置
BUILDING_SCALES = {
@@ -294,9 +294,7 @@ def generate_fire_evacuation_traffic(
# 在10个tick内陆续到达模拟疏散的紧急性
arrival_tick = alarm_tick + random.randint(0, min(10, duration - alarm_tick - 1))
if arrival_tick < duration:
traffic.append(
{"id": passenger_id, "origin": floor, "destination": 0, "tick": arrival_tick} # 疏散到大厅
)
traffic.append({"id": passenger_id, "origin": floor, "destination": 0, "tick": arrival_tick}) # 疏散到大厅
passenger_id += 1
return limit_traffic_count(traffic, max_people)
@@ -738,12 +736,12 @@ def determine_building_scale(floors: int, elevators: int) -> str:
return "large"
def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str] = None, **kwargs) -> int:
def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str] = None, **kwargs: Any) -> int:
"""生成单个流量文件,支持规模化配置"""
if scenario not in TRAFFIC_SCENARIOS:
raise ValueError(f"Unknown scenario: {scenario}. Available: {list(TRAFFIC_SCENARIOS.keys())}")
config = TRAFFIC_SCENARIOS[scenario]
config: Dict[str, Any] = TRAFFIC_SCENARIOS[scenario]
# 确定建筑规模
if scale is None:
@@ -766,6 +764,7 @@ def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str]
scale_params = config["scales"].get(scale, {})
# 合并参数kwargs > scale_params > building_scale_defaults
assert scale is not None # scale should be determined by this point
building_scale = BUILDING_SCALES[scale]
params = {}
@@ -786,9 +785,10 @@ def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str]
# 生成流量数据 - 只传递生成器函数需要的参数
import inspect
generator_signature = inspect.signature(config["generator"])
generator_func: Callable[..., List[Dict[str, Any]]] = config["generator"]
generator_signature = inspect.signature(generator_func)
generator_params = {k: v for k, v in params.items() if k in generator_signature.parameters}
traffic_data = config["generator"](**generator_params)
traffic_data = generator_func(**generator_params)
# 准备building配置
building_config = {
@@ -806,7 +806,7 @@ def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str]
complete_data = {"building": building_config, "traffic": traffic_data}
# 写入文件
with open(output_file, "w") as f:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(complete_data, f, indent=2, ensure_ascii=False)
print(f"Generated {len(traffic_data)} passengers for scenario '{scenario}' ({scale}) -> {output_file}")
@@ -819,7 +819,7 @@ def generate_scaled_traffic_files(
seed: int = 42,
generate_all_scales: bool = False,
custom_building: Optional[Dict[str, Any]] = None,
):
) -> None:
"""生成按规模分类的流量文件"""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
@@ -848,7 +848,7 @@ def generate_scaled_traffic_files(
def _generate_files_for_scale(
output_path: Path, scale: str, seed: int, custom_building: Optional[Dict[str, Any]] = None
):
) -> None:
"""为指定规模生成所有适合的场景文件"""
building_config = BUILDING_SCALES[scale]
total_passengers = 0
@@ -868,9 +868,10 @@ def _generate_files_for_scale(
print(f"\nGenerating {scale} scale traffic files:")
print(f"Building: {floors} floors, {elevators} elevators, capacity {elevator_capacity}")
for scenario_name, config in TRAFFIC_SCENARIOS.items():
for scenario_name, scenario_config in TRAFFIC_SCENARIOS.items():
# 检查场景是否适合该规模
if scale not in config["suitable_scales"]:
config_dict: Dict[str, Any] = scenario_config
if scale not in config_dict["suitable_scales"]:
continue
filename = f"{scenario_name}.json"
@@ -905,7 +906,7 @@ def generate_all_traffic_files(
elevators: int = 2,
elevator_capacity: int = 8,
seed: int = 42,
):
) -> None:
"""生成所有场景的流量文件 - 保持向后兼容"""
scale = determine_building_scale(floors, elevators)
custom_building = {"floors": floors, "elevators": elevators, "capacity": elevator_capacity}
@@ -913,7 +914,7 @@ def generate_all_traffic_files(
generate_scaled_traffic_files(output_dir=output_dir, scale=scale, seed=seed, custom_building=custom_building)
def main():
def main() -> None:
"""主函数 - 命令行接口"""
import argparse

View File

@@ -0,0 +1,410 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "high_density",
"scale": "medium",
"description": "高密度流量 - 压力测试 (medium规模)",
"expected_passengers": 66,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 4,
"destination": 0,
"tick": 1
},
{
"id": 2,
"origin": 4,
"destination": 2,
"tick": 2
},
{
"id": 3,
"origin": 4,
"destination": 3,
"tick": 3
},
{
"id": 4,
"origin": 0,
"destination": 4,
"tick": 4
},
{
"id": 5,
"origin": 5,
"destination": 0,
"tick": 14
},
{
"id": 6,
"origin": 1,
"destination": 3,
"tick": 18
},
{
"id": 7,
"origin": 2,
"destination": 3,
"tick": 19
},
{
"id": 8,
"origin": 5,
"destination": 2,
"tick": 20
},
{
"id": 9,
"origin": 0,
"destination": 2,
"tick": 21
},
{
"id": 10,
"origin": 2,
"destination": 3,
"tick": 22
},
{
"id": 11,
"origin": 2,
"destination": 4,
"tick": 23
},
{
"id": 12,
"origin": 2,
"destination": 4,
"tick": 25
},
{
"id": 13,
"origin": 3,
"destination": 4,
"tick": 30
},
{
"id": 14,
"origin": 3,
"destination": 4,
"tick": 32
},
{
"id": 15,
"origin": 4,
"destination": 1,
"tick": 34
},
{
"id": 16,
"origin": 2,
"destination": 3,
"tick": 35
},
{
"id": 17,
"origin": 5,
"destination": 0,
"tick": 36
},
{
"id": 18,
"origin": 1,
"destination": 2,
"tick": 38
},
{
"id": 19,
"origin": 0,
"destination": 4,
"tick": 40
},
{
"id": 20,
"origin": 0,
"destination": 1,
"tick": 50
},
{
"id": 21,
"origin": 1,
"destination": 2,
"tick": 54
},
{
"id": 22,
"origin": 5,
"destination": 1,
"tick": 60
},
{
"id": 23,
"origin": 3,
"destination": 1,
"tick": 61
},
{
"id": 24,
"origin": 3,
"destination": 1,
"tick": 63
},
{
"id": 25,
"origin": 0,
"destination": 1,
"tick": 67
},
{
"id": 26,
"origin": 0,
"destination": 4,
"tick": 70
},
{
"id": 27,
"origin": 5,
"destination": 3,
"tick": 76
},
{
"id": 28,
"origin": 2,
"destination": 4,
"tick": 78
},
{
"id": 29,
"origin": 2,
"destination": 3,
"tick": 80
},
{
"id": 30,
"origin": 5,
"destination": 1,
"tick": 91
},
{
"id": 31,
"origin": 3,
"destination": 1,
"tick": 92
},
{
"id": 32,
"origin": 3,
"destination": 4,
"tick": 95
},
{
"id": 33,
"origin": 0,
"destination": 1,
"tick": 96
},
{
"id": 34,
"origin": 2,
"destination": 5,
"tick": 98
},
{
"id": 35,
"origin": 2,
"destination": 0,
"tick": 100
},
{
"id": 36,
"origin": 1,
"destination": 4,
"tick": 103
},
{
"id": 37,
"origin": 2,
"destination": 5,
"tick": 110
},
{
"id": 38,
"origin": 3,
"destination": 5,
"tick": 112
},
{
"id": 39,
"origin": 0,
"destination": 5,
"tick": 113
},
{
"id": 40,
"origin": 3,
"destination": 1,
"tick": 114
},
{
"id": 41,
"origin": 0,
"destination": 5,
"tick": 122
},
{
"id": 42,
"origin": 2,
"destination": 5,
"tick": 127
},
{
"id": 43,
"origin": 1,
"destination": 4,
"tick": 130
},
{
"id": 44,
"origin": 4,
"destination": 2,
"tick": 137
},
{
"id": 45,
"origin": 3,
"destination": 4,
"tick": 139
},
{
"id": 46,
"origin": 4,
"destination": 0,
"tick": 140
},
{
"id": 47,
"origin": 4,
"destination": 2,
"tick": 143
},
{
"id": 48,
"origin": 2,
"destination": 4,
"tick": 144
},
{
"id": 49,
"origin": 2,
"destination": 5,
"tick": 147
},
{
"id": 50,
"origin": 5,
"destination": 1,
"tick": 148
},
{
"id": 51,
"origin": 1,
"destination": 3,
"tick": 149
},
{
"id": 52,
"origin": 4,
"destination": 1,
"tick": 160
},
{
"id": 53,
"origin": 4,
"destination": 0,
"tick": 172
},
{
"id": 54,
"origin": 3,
"destination": 1,
"tick": 173
},
{
"id": 55,
"origin": 5,
"destination": 4,
"tick": 174
},
{
"id": 56,
"origin": 0,
"destination": 4,
"tick": 179
},
{
"id": 57,
"origin": 4,
"destination": 2,
"tick": 180
},
{
"id": 58,
"origin": 2,
"destination": 1,
"tick": 182
},
{
"id": 59,
"origin": 1,
"destination": 0,
"tick": 186
},
{
"id": 60,
"origin": 2,
"destination": 0,
"tick": 188
},
{
"id": 61,
"origin": 4,
"destination": 5,
"tick": 191
},
{
"id": 62,
"origin": 5,
"destination": 1,
"tick": 192
},
{
"id": 63,
"origin": 1,
"destination": 4,
"tick": 193
},
{
"id": 64,
"origin": 0,
"destination": 3,
"tick": 195
},
{
"id": 65,
"origin": 4,
"destination": 2,
"tick": 196
},
{
"id": 66,
"origin": 5,
"destination": 0,
"tick": 197
}
]
}

View File

@@ -1,470 +1,374 @@
{
"building": {
"floors": 12,
"elevators": 4,
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "inter_floor",
"scale": "large",
"description": "楼层间流量 - 适合小建筑 (large规模)",
"expected_passengers": 76,
"duration": 100
"scale": "medium",
"description": "楼层间流量 - 适合小建筑 (medium规模)",
"expected_passengers": 60,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 0,
"origin": 5,
"destination": 1,
"tick": 1
"tick": 0
},
{
"id": 2,
"origin": 2,
"destination": 6,
"tick": 11
"origin": 5,
"destination": 1,
"tick": 5
},
{
"id": 3,
"origin": 11,
"destination": 3,
"tick": 12
"origin": 1,
"destination": 5,
"tick": 9
},
{
"id": 4,
"origin": 11,
"destination": 10,
"tick": 13
"origin": 1,
"destination": 3,
"tick": 11
},
{
"id": 5,
"origin": 1,
"destination": 4,
"tick": 20
"origin": 5,
"destination": 1,
"tick": 13
},
{
"id": 6,
"origin": 1,
"destination": 2,
"tick": 22
"origin": 2,
"destination": 3,
"tick": 17
},
{
"id": 7,
"origin": 6,
"destination": 3,
"tick": 26
"origin": 3,
"destination": 2,
"tick": 18
},
{
"id": 8,
"origin": 8,
"destination": 9,
"tick": 27
"origin": 4,
"destination": 3,
"tick": 19
},
{
"id": 9,
"origin": 1,
"destination": 8,
"tick": 28
"origin": 4,
"destination": 1,
"tick": 20
},
{
"id": 10,
"origin": 9,
"destination": 3,
"tick": 40
"origin": 4,
"destination": 1,
"tick": 21
},
{
"id": 11,
"origin": 9,
"destination": 3,
"tick": 52
"origin": 3,
"destination": 1,
"tick": 22
},
{
"id": 12,
"origin": 8,
"destination": 10,
"tick": 57
"origin": 4,
"destination": 3,
"tick": 24
},
{
"id": 13,
"origin": 10,
"destination": 9,
"tick": 58
"origin": 4,
"destination": 1,
"tick": 25
},
{
"id": 14,
"origin": 4,
"destination": 1,
"tick": 60
"destination": 3,
"tick": 26
},
{
"id": 15,
"origin": 7,
"destination": 8,
"tick": 65
"origin": 4,
"destination": 2,
"tick": 28
},
{
"id": 16,
"origin": 11,
"destination": 1,
"tick": 68
"origin": 3,
"destination": 2,
"tick": 29
},
{
"id": 17,
"origin": 5,
"destination": 7,
"tick": 69
"destination": 3,
"tick": 34
},
{
"id": 18,
"origin": 8,
"destination": 9,
"tick": 71
"origin": 3,
"destination": 1,
"tick": 36
},
{
"id": 19,
"origin": 3,
"destination": 9,
"tick": 75
"destination": 2,
"tick": 38
},
{
"id": 20,
"origin": 3,
"destination": 8,
"tick": 90
"origin": 2,
"destination": 5,
"tick": 43
},
{
"id": 21,
"origin": 2,
"destination": 4,
"tick": 91
"origin": 3,
"destination": 2,
"tick": 46
},
{
"id": 22,
"origin": 8,
"destination": 9,
"tick": 94
"origin": 4,
"destination": 1,
"tick": 51
},
{
"id": 23,
"origin": 5,
"destination": 8,
"tick": 97
"destination": 4,
"tick": 52
},
{
"id": 24,
"origin": 1,
"destination": 3,
"tick": 101
"origin": 5,
"destination": 1,
"tick": 54
},
{
"id": 25,
"origin": 2,
"origin": 1,
"destination": 4,
"tick": 106
"tick": 58
},
{
"id": 26,
"origin": 2,
"destination": 10,
"tick": 107
"origin": 4,
"destination": 3,
"tick": 62
},
{
"id": 27,
"origin": 4,
"destination": 7,
"tick": 111
"origin": 2,
"destination": 3,
"tick": 63
},
{
"id": 28,
"origin": 2,
"destination": 7,
"tick": 112
"destination": 4,
"tick": 66
},
{
"id": 29,
"origin": 8,
"destination": 6,
"tick": 115
"origin": 3,
"destination": 5,
"tick": 68
},
{
"id": 30,
"origin": 8,
"destination": 9,
"tick": 116
"origin": 4,
"destination": 1,
"tick": 69
},
{
"id": 31,
"origin": 7,
"destination": 3,
"tick": 118
"origin": 1,
"destination": 2,
"tick": 71
},
{
"id": 32,
"origin": 6,
"destination": 7,
"tick": 121
"origin": 1,
"destination": 3,
"tick": 79
},
{
"id": 33,
"origin": 2,
"destination": 11,
"tick": 123
"origin": 5,
"destination": 1,
"tick": 81
},
{
"id": 34,
"origin": 6,
"origin": 4,
"destination": 1,
"tick": 128
"tick": 83
},
{
"id": 35,
"origin": 8,
"destination": 7,
"tick": 129
"origin": 5,
"destination": 2,
"tick": 90
},
{
"id": 36,
"origin": 6,
"destination": 10,
"tick": 131
"origin": 2,
"destination": 1,
"tick": 93
},
{
"id": 37,
"origin": 8,
"origin": 2,
"destination": 4,
"tick": 135
"tick": 94
},
{
"id": 38,
"origin": 10,
"destination": 6,
"tick": 137
"origin": 2,
"destination": 3,
"tick": 102
},
{
"id": 39,
"origin": 10,
"origin": 5,
"destination": 1,
"tick": 105
},
{
"id": 40,
"origin": 4,
"destination": 5,
"tick": 106
},
{
"id": 41,
"origin": 3,
"destination": 4,
"tick": 109
},
{
"id": 42,
"origin": 4,
"destination": 3,
"tick": 122
},
{
"id": 43,
"origin": 5,
"destination": 2,
"tick": 127
},
{
"id": 44,
"origin": 5,
"destination": 2,
"tick": 130
},
{
"id": 45,
"origin": 1,
"destination": 2,
"tick": 135
},
{
"id": 46,
"origin": 5,
"destination": 3,
"tick": 139
},
{
"id": 47,
"origin": 1,
"destination": 5,
"tick": 144
},
{
"id": 48,
"origin": 5,
"destination": 2,
"tick": 146
},
{
"id": 49,
"origin": 3,
"destination": 4,
"tick": 151
},
{
"id": 50,
"origin": 2,
"destination": 4,
"tick": 155
},
{
"id": 51,
"origin": 3,
"destination": 5,
"tick": 157
},
{
"id": 40,
"origin": 1,
"destination": 8,
"tick": 158
},
{
"id": 41,
"origin": 6,
"destination": 11,
"tick": 159
},
{
"id": 42,
"origin": 7,
"destination": 5,
"tick": 161
},
{
"id": 43,
"origin": 6,
"destination": 4,
"tick": 162
},
{
"id": 44,
"origin": 3,
"destination": 8,
"id": 52,
"origin": 5,
"destination": 2,
"tick": 163
},
{
"id": 45,
"origin": 6,
"destination": 9,
"tick": 165
},
{
"id": 46,
"origin": 6,
"destination": 5,
"id": 53,
"origin": 3,
"destination": 1,
"tick": 172
},
{
"id": 47,
"id": 54,
"origin": 2,
"destination": 8,
"tick": 173
},
{
"id": 48,
"origin": 9,
"destination": 10,
"destination": 4,
"tick": 174
},
{
"id": 49,
"origin": 1,
"destination": 10,
"tick": 175
},
{
"id": 50,
"origin": 3,
"destination": 5,
"tick": 176
},
{
"id": 51,
"origin": 1,
"destination": 8,
"id": 55,
"origin": 4,
"destination": 1,
"tick": 179
},
{
"id": 52,
"origin": 5,
"destination": 4,
"tick": 184
},
{
"id": 53,
"origin": 7,
"destination": 10,
"tick": 186
},
{
"id": 54,
"origin": 8,
"destination": 2,
"tick": 194
},
{
"id": 55,
"origin": 5,
"destination": 1,
"tick": 200
},
{
"id": 56,
"origin": 10,
"destination": 2,
"tick": 204
"origin": 3,
"destination": 4,
"tick": 186
},
{
"id": 57,
"origin": 5,
"destination": 2,
"tick": 205
"tick": 189
},
{
"id": 58,
"origin": 7,
"destination": 11,
"tick": 210
"origin": 1,
"destination": 4,
"tick": 190
},
{
"id": 59,
"origin": 11,
"destination": 4,
"tick": 212
"origin": 5,
"destination": 2,
"tick": 194
},
{
"id": 60,
"origin": 4,
"destination": 7,
"tick": 230
},
{
"id": 61,
"origin": 8,
"destination": 6,
"tick": 239
},
{
"id": 62,
"origin": 7,
"destination": 3,
"tick": 249
},
{
"id": 63,
"origin": 6,
"destination": 1,
"tick": 250
},
{
"id": 64,
"origin": 5,
"destination": 4,
"tick": 254
},
{
"id": 65,
"origin": 9,
"destination": 10,
"tick": 256
},
{
"id": 66,
"origin": 4,
"destination": 7,
"tick": 267
},
{
"id": 67,
"origin": 11,
"destination": 4,
"tick": 269
},
{
"id": 68,
"origin": 3,
"destination": 8,
"tick": 272
},
{
"id": 69,
"origin": 8,
"destination": 10,
"tick": 278
},
{
"id": 70,
"origin": 10,
"destination": 3,
"tick": 279
},
{
"id": 71,
"origin": 11,
"destination": 2,
"tick": 284
},
{
"id": 72,
"origin": 10,
"destination": 11,
"tick": 288
},
{
"id": 73,
"origin": 7,
"destination": 2,
"tick": 291
},
{
"id": 74,
"origin": 2,
"destination": 9,
"tick": 294
},
{
"id": 75,
"origin": 7,
"destination": 9,
"tick": 295
},
{
"id": 76,
"origin": 7,
"destination": 8,
"tick": 297
"destination": 4,
"tick": 197
}
]
}
}

View File

@@ -0,0 +1,164 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "lunch_rush",
"scale": "medium",
"description": "午餐时间流量 - 双向流量,适合中大型建筑 (medium规模)",
"expected_passengers": 25,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 4,
"destination": 2,
"tick": 4
},
{
"id": 2,
"origin": 4,
"destination": 2,
"tick": 21
},
{
"id": 3,
"origin": 3,
"destination": 2,
"tick": 44
},
{
"id": 4,
"origin": 1,
"destination": 3,
"tick": 55
},
{
"id": 5,
"origin": 1,
"destination": 3,
"tick": 63
},
{
"id": 6,
"origin": 1,
"destination": 4,
"tick": 64
},
{
"id": 7,
"origin": 1,
"destination": 4,
"tick": 72
},
{
"id": 8,
"origin": 1,
"destination": 4,
"tick": 75
},
{
"id": 9,
"origin": 2,
"destination": 4,
"tick": 78
},
{
"id": 10,
"origin": 5,
"destination": 1,
"tick": 80
},
{
"id": 11,
"origin": 4,
"destination": 2,
"tick": 82
},
{
"id": 12,
"origin": 5,
"destination": 1,
"tick": 85
},
{
"id": 13,
"origin": 5,
"destination": 2,
"tick": 93
},
{
"id": 14,
"origin": 2,
"destination": 3,
"tick": 96
},
{
"id": 15,
"origin": 2,
"destination": 3,
"tick": 97
},
{
"id": 16,
"origin": 2,
"destination": 5,
"tick": 104
},
{
"id": 17,
"origin": 3,
"destination": 1,
"tick": 107
},
{
"id": 18,
"origin": 2,
"destination": 3,
"tick": 111
},
{
"id": 19,
"origin": 3,
"destination": 1,
"tick": 113
},
{
"id": 20,
"origin": 2,
"destination": 3,
"tick": 114
},
{
"id": 21,
"origin": 3,
"destination": 2,
"tick": 116
},
{
"id": 22,
"origin": 2,
"destination": 3,
"tick": 127
},
{
"id": 23,
"origin": 2,
"destination": 3,
"tick": 139
},
{
"id": 24,
"origin": 2,
"destination": 5,
"tick": 166
},
{
"id": 25,
"origin": 4,
"destination": 1,
"tick": 192
}
]
}

View File

@@ -0,0 +1,494 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "medical",
"scale": "medium",
"description": "医疗建筑 - 特殊流量模式 (medium规模)",
"expected_passengers": 80,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 1,
"destination": 0,
"tick": 0
},
{
"id": 2,
"origin": 0,
"destination": 1,
"tick": 3
},
{
"id": 3,
"origin": 4,
"destination": 0,
"tick": 4
},
{
"id": 4,
"origin": 0,
"destination": 1,
"tick": 7
},
{
"id": 5,
"origin": 3,
"destination": 0,
"tick": 8
},
{
"id": 6,
"origin": 0,
"destination": 2,
"tick": 9
},
{
"id": 7,
"origin": 0,
"destination": 3,
"tick": 12
},
{
"id": 8,
"origin": 0,
"destination": 1,
"tick": 13
},
{
"id": 9,
"origin": 2,
"destination": 0,
"tick": 16
},
{
"id": 10,
"origin": 1,
"destination": 3,
"tick": 17
},
{
"id": 11,
"origin": 0,
"destination": 3,
"tick": 21
},
{
"id": 12,
"origin": 0,
"destination": 3,
"tick": 24
},
{
"id": 13,
"origin": 0,
"destination": 1,
"tick": 30
},
{
"id": 14,
"origin": 0,
"destination": 4,
"tick": 31
},
{
"id": 15,
"origin": 0,
"destination": 1,
"tick": 32
},
{
"id": 16,
"origin": 0,
"destination": 1,
"tick": 34
},
{
"id": 17,
"origin": 4,
"destination": 0,
"tick": 35
},
{
"id": 18,
"origin": 0,
"destination": 4,
"tick": 36
},
{
"id": 19,
"origin": 0,
"destination": 2,
"tick": 37
},
{
"id": 20,
"origin": 0,
"destination": 1,
"tick": 38
},
{
"id": 21,
"origin": 4,
"destination": 0,
"tick": 39
},
{
"id": 22,
"origin": 0,
"destination": 3,
"tick": 40
},
{
"id": 23,
"origin": 0,
"destination": 1,
"tick": 41
},
{
"id": 24,
"origin": 1,
"destination": 0,
"tick": 43
},
{
"id": 25,
"origin": 1,
"destination": 0,
"tick": 45
},
{
"id": 26,
"origin": 0,
"destination": 1,
"tick": 48
},
{
"id": 27,
"origin": 0,
"destination": 4,
"tick": 49
},
{
"id": 28,
"origin": 0,
"destination": 2,
"tick": 50
},
{
"id": 29,
"origin": 0,
"destination": 2,
"tick": 51
},
{
"id": 30,
"origin": 0,
"destination": 2,
"tick": 52
},
{
"id": 31,
"origin": 0,
"destination": 1,
"tick": 53
},
{
"id": 32,
"origin": 1,
"destination": 0,
"tick": 55
},
{
"id": 33,
"origin": 0,
"destination": 2,
"tick": 56
},
{
"id": 34,
"origin": 2,
"destination": 0,
"tick": 57
},
{
"id": 35,
"origin": 0,
"destination": 2,
"tick": 63
},
{
"id": 36,
"origin": 0,
"destination": 3,
"tick": 64
},
{
"id": 37,
"origin": 0,
"destination": 1,
"tick": 65
},
{
"id": 38,
"origin": 0,
"destination": 2,
"tick": 66
},
{
"id": 39,
"origin": 1,
"destination": 0,
"tick": 68
},
{
"id": 40,
"origin": 1,
"destination": 0,
"tick": 69
},
{
"id": 41,
"origin": 0,
"destination": 2,
"tick": 70
},
{
"id": 42,
"origin": 1,
"destination": 3,
"tick": 72
},
{
"id": 43,
"origin": 4,
"destination": 0,
"tick": 73
},
{
"id": 44,
"origin": 0,
"destination": 2,
"tick": 74
},
{
"id": 45,
"origin": 2,
"destination": 0,
"tick": 75
},
{
"id": 46,
"origin": 0,
"destination": 1,
"tick": 79
},
{
"id": 47,
"origin": 1,
"destination": 0,
"tick": 80
},
{
"id": 48,
"origin": 0,
"destination": 1,
"tick": 83
},
{
"id": 49,
"origin": 0,
"destination": 1,
"tick": 84
},
{
"id": 50,
"origin": 0,
"destination": 1,
"tick": 85
},
{
"id": 51,
"origin": 5,
"destination": 3,
"tick": 86
},
{
"id": 52,
"origin": 0,
"destination": 1,
"tick": 87
},
{
"id": 53,
"origin": 1,
"destination": 0,
"tick": 88
},
{
"id": 54,
"origin": 0,
"destination": 5,
"tick": 89
},
{
"id": 55,
"origin": 0,
"destination": 4,
"tick": 91
},
{
"id": 56,
"origin": 0,
"destination": 5,
"tick": 92
},
{
"id": 57,
"origin": 3,
"destination": 0,
"tick": 93
},
{
"id": 58,
"origin": 1,
"destination": 0,
"tick": 94
},
{
"id": 59,
"origin": 2,
"destination": 0,
"tick": 95
},
{
"id": 60,
"origin": 1,
"destination": 0,
"tick": 98
},
{
"id": 61,
"origin": 2,
"destination": 0,
"tick": 104
},
{
"id": 62,
"origin": 1,
"destination": 0,
"tick": 105
},
{
"id": 63,
"origin": 0,
"destination": 4,
"tick": 106
},
{
"id": 64,
"origin": 2,
"destination": 0,
"tick": 107
},
{
"id": 65,
"origin": 2,
"destination": 0,
"tick": 108
},
{
"id": 66,
"origin": 0,
"destination": 1,
"tick": 109
},
{
"id": 67,
"origin": 1,
"destination": 0,
"tick": 110
},
{
"id": 68,
"origin": 0,
"destination": 2,
"tick": 112
},
{
"id": 69,
"origin": 1,
"destination": 0,
"tick": 114
},
{
"id": 70,
"origin": 0,
"destination": 4,
"tick": 117
},
{
"id": 71,
"origin": 0,
"destination": 1,
"tick": 118
},
{
"id": 72,
"origin": 0,
"destination": 3,
"tick": 119
},
{
"id": 73,
"origin": 0,
"destination": 2,
"tick": 120
},
{
"id": 74,
"origin": 2,
"destination": 0,
"tick": 122
},
{
"id": 75,
"origin": 0,
"destination": 2,
"tick": 128
},
{
"id": 76,
"origin": 0,
"destination": 4,
"tick": 129
},
{
"id": 77,
"origin": 0,
"destination": 2,
"tick": 132
},
{
"id": 78,
"origin": 3,
"destination": 5,
"tick": 135
},
{
"id": 79,
"origin": 0,
"destination": 3,
"tick": 137
},
{
"id": 80,
"origin": 0,
"destination": 4,
"tick": 138
}
]
}

View File

@@ -0,0 +1,314 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "meeting_event",
"scale": "medium",
"description": "会议事件 - 集中到达和离开 (medium规模)",
"expected_passengers": 50,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 0,
"destination": 3,
"tick": 0
},
{
"id": 2,
"origin": 0,
"destination": 3,
"tick": 1
},
{
"id": 3,
"origin": 0,
"destination": 3,
"tick": 2
},
{
"id": 4,
"origin": 0,
"destination": 3,
"tick": 3
},
{
"id": 5,
"origin": 0,
"destination": 3,
"tick": 4
},
{
"id": 6,
"origin": 0,
"destination": 3,
"tick": 5
},
{
"id": 7,
"origin": 3,
"destination": 1,
"tick": 6
},
{
"id": 8,
"origin": 0,
"destination": 3,
"tick": 7
},
{
"id": 9,
"origin": 0,
"destination": 3,
"tick": 8
},
{
"id": 10,
"origin": 0,
"destination": 1,
"tick": 9
},
{
"id": 11,
"origin": 0,
"destination": 3,
"tick": 10
},
{
"id": 12,
"origin": 0,
"destination": 3,
"tick": 11
},
{
"id": 13,
"origin": 0,
"destination": 3,
"tick": 12
},
{
"id": 14,
"origin": 0,
"destination": 3,
"tick": 13
},
{
"id": 15,
"origin": 0,
"destination": 3,
"tick": 14
},
{
"id": 16,
"origin": 0,
"destination": 3,
"tick": 15
},
{
"id": 17,
"origin": 0,
"destination": 3,
"tick": 16
},
{
"id": 18,
"origin": 0,
"destination": 3,
"tick": 17
},
{
"id": 19,
"origin": 0,
"destination": 3,
"tick": 18
},
{
"id": 20,
"origin": 0,
"destination": 3,
"tick": 19
},
{
"id": 21,
"origin": 0,
"destination": 3,
"tick": 20
},
{
"id": 22,
"origin": 0,
"destination": 3,
"tick": 21
},
{
"id": 23,
"origin": 1,
"destination": 2,
"tick": 22
},
{
"id": 24,
"origin": 0,
"destination": 3,
"tick": 23
},
{
"id": 25,
"origin": 0,
"destination": 3,
"tick": 24
},
{
"id": 26,
"origin": 0,
"destination": 3,
"tick": 25
},
{
"id": 27,
"origin": 0,
"destination": 3,
"tick": 26
},
{
"id": 28,
"origin": 0,
"destination": 3,
"tick": 27
},
{
"id": 29,
"origin": 0,
"destination": 3,
"tick": 28
},
{
"id": 30,
"origin": 0,
"destination": 3,
"tick": 29
},
{
"id": 31,
"origin": 0,
"destination": 3,
"tick": 30
},
{
"id": 32,
"origin": 0,
"destination": 3,
"tick": 31
},
{
"id": 33,
"origin": 0,
"destination": 3,
"tick": 32
},
{
"id": 34,
"origin": 0,
"destination": 3,
"tick": 33
},
{
"id": 35,
"origin": 0,
"destination": 3,
"tick": 34
},
{
"id": 36,
"origin": 4,
"destination": 2,
"tick": 35
},
{
"id": 37,
"origin": 0,
"destination": 3,
"tick": 36
},
{
"id": 38,
"origin": 0,
"destination": 3,
"tick": 37
},
{
"id": 39,
"origin": 0,
"destination": 3,
"tick": 38
},
{
"id": 40,
"origin": 0,
"destination": 3,
"tick": 39
},
{
"id": 41,
"origin": 0,
"destination": 3,
"tick": 40
},
{
"id": 42,
"origin": 0,
"destination": 3,
"tick": 41
},
{
"id": 43,
"origin": 0,
"destination": 3,
"tick": 42
},
{
"id": 44,
"origin": 0,
"destination": 3,
"tick": 43
},
{
"id": 45,
"origin": 0,
"destination": 3,
"tick": 44
},
{
"id": 46,
"origin": 5,
"destination": 2,
"tick": 45
},
{
"id": 47,
"origin": 0,
"destination": 3,
"tick": 46
},
{
"id": 48,
"origin": 0,
"destination": 3,
"tick": 47
},
{
"id": 49,
"origin": 0,
"destination": 5,
"tick": 48
},
{
"id": 50,
"origin": 0,
"destination": 3,
"tick": 49
}
]
}

View File

@@ -0,0 +1,614 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "mixed_scenario",
"scale": "medium",
"description": "混合场景 - 包含多种流量模式,适合中大型建筑 (medium规模)",
"expected_passengers": 100,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 0,
"destination": 4,
"tick": 0
},
{
"id": 2,
"origin": 0,
"destination": 5,
"tick": 2
},
{
"id": 3,
"origin": 0,
"destination": 2,
"tick": 4
},
{
"id": 4,
"origin": 0,
"destination": 4,
"tick": 6
},
{
"id": 5,
"origin": 0,
"destination": 1,
"tick": 11
},
{
"id": 6,
"origin": 0,
"destination": 2,
"tick": 12
},
{
"id": 7,
"origin": 3,
"destination": 4,
"tick": 14
},
{
"id": 8,
"origin": 0,
"destination": 4,
"tick": 15
},
{
"id": 9,
"origin": 0,
"destination": 2,
"tick": 16
},
{
"id": 10,
"origin": 0,
"destination": 2,
"tick": 17
},
{
"id": 11,
"origin": 0,
"destination": 3,
"tick": 21
},
{
"id": 12,
"origin": 0,
"destination": 4,
"tick": 22
},
{
"id": 13,
"origin": 0,
"destination": 1,
"tick": 23
},
{
"id": 14,
"origin": 0,
"destination": 4,
"tick": 25
},
{
"id": 15,
"origin": 0,
"destination": 1,
"tick": 27
},
{
"id": 16,
"origin": 0,
"destination": 3,
"tick": 28
},
{
"id": 17,
"origin": 0,
"destination": 1,
"tick": 29
},
{
"id": 18,
"origin": 0,
"destination": 3,
"tick": 31
},
{
"id": 19,
"origin": 0,
"destination": 3,
"tick": 32
},
{
"id": 20,
"origin": 0,
"destination": 1,
"tick": 40
},
{
"id": 21,
"origin": 0,
"destination": 3,
"tick": 41
},
{
"id": 22,
"origin": 0,
"destination": 1,
"tick": 42
},
{
"id": 23,
"origin": 0,
"destination": 5,
"tick": 43
},
{
"id": 24,
"origin": 0,
"destination": 2,
"tick": 44
},
{
"id": 25,
"origin": 0,
"destination": 4,
"tick": 49
},
{
"id": 26,
"origin": 0,
"destination": 1,
"tick": 50
},
{
"id": 27,
"origin": 2,
"destination": 1,
"tick": 52
},
{
"id": 28,
"origin": 2,
"destination": 3,
"tick": 53
},
{
"id": 29,
"origin": 4,
"destination": 0,
"tick": 54
},
{
"id": 30,
"origin": 5,
"destination": 2,
"tick": 55
},
{
"id": 31,
"origin": 5,
"destination": 1,
"tick": 57
},
{
"id": 32,
"origin": 2,
"destination": 5,
"tick": 58
},
{
"id": 33,
"origin": 0,
"destination": 1,
"tick": 66
},
{
"id": 34,
"origin": 3,
"destination": 1,
"tick": 68
},
{
"id": 35,
"origin": 2,
"destination": 5,
"tick": 72
},
{
"id": 36,
"origin": 4,
"destination": 2,
"tick": 73
},
{
"id": 37,
"origin": 0,
"destination": 4,
"tick": 74
},
{
"id": 38,
"origin": 3,
"destination": 0,
"tick": 75
},
{
"id": 39,
"origin": 3,
"destination": 2,
"tick": 80
},
{
"id": 40,
"origin": 3,
"destination": 4,
"tick": 81
},
{
"id": 41,
"origin": 0,
"destination": 4,
"tick": 85
},
{
"id": 42,
"origin": 4,
"destination": 2,
"tick": 87
},
{
"id": 43,
"origin": 3,
"destination": 5,
"tick": 90
},
{
"id": 44,
"origin": 5,
"destination": 1,
"tick": 91
},
{
"id": 45,
"origin": 2,
"destination": 4,
"tick": 92
},
{
"id": 46,
"origin": 3,
"destination": 0,
"tick": 93
},
{
"id": 47,
"origin": 3,
"destination": 5,
"tick": 95
},
{
"id": 48,
"origin": 0,
"destination": 5,
"tick": 97
},
{
"id": 49,
"origin": 2,
"destination": 5,
"tick": 98
},
{
"id": 50,
"origin": 3,
"destination": 2,
"tick": 100
},
{
"id": 51,
"origin": 1,
"destination": 5,
"tick": 101
},
{
"id": 52,
"origin": 3,
"destination": 1,
"tick": 102
},
{
"id": 53,
"origin": 0,
"destination": 3,
"tick": 103
},
{
"id": 54,
"origin": 3,
"destination": 1,
"tick": 104
},
{
"id": 55,
"origin": 2,
"destination": 5,
"tick": 106
},
{
"id": 56,
"origin": 5,
"destination": 4,
"tick": 107
},
{
"id": 57,
"origin": 5,
"destination": 2,
"tick": 108
},
{
"id": 58,
"origin": 3,
"destination": 2,
"tick": 109
},
{
"id": 59,
"origin": 2,
"destination": 4,
"tick": 110
},
{
"id": 60,
"origin": 4,
"destination": 2,
"tick": 111
},
{
"id": 61,
"origin": 0,
"destination": 5,
"tick": 112
},
{
"id": 62,
"origin": 2,
"destination": 4,
"tick": 114
},
{
"id": 63,
"origin": 4,
"destination": 2,
"tick": 115
},
{
"id": 64,
"origin": 4,
"destination": 2,
"tick": 116
},
{
"id": 65,
"origin": 1,
"destination": 4,
"tick": 117
},
{
"id": 66,
"origin": 2,
"destination": 3,
"tick": 118
},
{
"id": 67,
"origin": 5,
"destination": 1,
"tick": 119
},
{
"id": 68,
"origin": 0,
"destination": 3,
"tick": 121
},
{
"id": 69,
"origin": 3,
"destination": 5,
"tick": 123
},
{
"id": 70,
"origin": 1,
"destination": 3,
"tick": 124
},
{
"id": 71,
"origin": 5,
"destination": 3,
"tick": 125
},
{
"id": 72,
"origin": 5,
"destination": 1,
"tick": 126
},
{
"id": 73,
"origin": 5,
"destination": 1,
"tick": 127
},
{
"id": 74,
"origin": 3,
"destination": 1,
"tick": 129
},
{
"id": 75,
"origin": 3,
"destination": 2,
"tick": 130
},
{
"id": 76,
"origin": 2,
"destination": 1,
"tick": 131
},
{
"id": 77,
"origin": 3,
"destination": 0,
"tick": 132
},
{
"id": 78,
"origin": 3,
"destination": 0,
"tick": 139
},
{
"id": 79,
"origin": 4,
"destination": 0,
"tick": 143
},
{
"id": 80,
"origin": 2,
"destination": 1,
"tick": 144
},
{
"id": 81,
"origin": 2,
"destination": 0,
"tick": 145
},
{
"id": 82,
"origin": 1,
"destination": 0,
"tick": 148
},
{
"id": 83,
"origin": 3,
"destination": 1,
"tick": 150
},
{
"id": 84,
"origin": 3,
"destination": 0,
"tick": 152
},
{
"id": 85,
"origin": 3,
"destination": 0,
"tick": 153
},
{
"id": 86,
"origin": 4,
"destination": 0,
"tick": 154
},
{
"id": 87,
"origin": 4,
"destination": 0,
"tick": 159
},
{
"id": 88,
"origin": 3,
"destination": 0,
"tick": 160
},
{
"id": 89,
"origin": 2,
"destination": 0,
"tick": 161
},
{
"id": 90,
"origin": 2,
"destination": 1,
"tick": 162
},
{
"id": 91,
"origin": 2,
"destination": 0,
"tick": 164
},
{
"id": 92,
"origin": 3,
"destination": 1,
"tick": 166
},
{
"id": 93,
"origin": 5,
"destination": 0,
"tick": 169
},
{
"id": 94,
"origin": 2,
"destination": 0,
"tick": 171
},
{
"id": 95,
"origin": 3,
"destination": 0,
"tick": 173
},
{
"id": 96,
"origin": 5,
"destination": 0,
"tick": 175
},
{
"id": 97,
"origin": 3,
"destination": 0,
"tick": 176
},
{
"id": 98,
"origin": 5,
"destination": 0,
"tick": 178
},
{
"id": 99,
"origin": 5,
"destination": 0,
"tick": 181
},
{
"id": 100,
"origin": 2,
"destination": 0,
"tick": 190
}
]
}

View File

@@ -0,0 +1,590 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "progressive_test",
"scale": "medium",
"description": "渐进式测试 - 强度逐渐增加 (medium规模)",
"expected_passengers": 96,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 1,
"destination": 0,
"tick": 2
},
{
"id": 2,
"origin": 0,
"destination": 3,
"tick": 4
},
{
"id": 3,
"origin": 0,
"destination": 5,
"tick": 10
},
{
"id": 4,
"origin": 0,
"destination": 1,
"tick": 11
},
{
"id": 5,
"origin": 0,
"destination": 1,
"tick": 13
},
{
"id": 6,
"origin": 2,
"destination": 1,
"tick": 14
},
{
"id": 7,
"origin": 3,
"destination": 5,
"tick": 16
},
{
"id": 8,
"origin": 1,
"destination": 4,
"tick": 17
},
{
"id": 9,
"origin": 5,
"destination": 3,
"tick": 18
},
{
"id": 10,
"origin": 3,
"destination": 4,
"tick": 22
},
{
"id": 11,
"origin": 5,
"destination": 0,
"tick": 23
},
{
"id": 12,
"origin": 0,
"destination": 5,
"tick": 24
},
{
"id": 13,
"origin": 4,
"destination": 5,
"tick": 26
},
{
"id": 14,
"origin": 3,
"destination": 2,
"tick": 29
},
{
"id": 15,
"origin": 4,
"destination": 3,
"tick": 30
},
{
"id": 16,
"origin": 4,
"destination": 1,
"tick": 32
},
{
"id": 17,
"origin": 0,
"destination": 1,
"tick": 35
},
{
"id": 18,
"origin": 2,
"destination": 0,
"tick": 38
},
{
"id": 19,
"origin": 5,
"destination": 3,
"tick": 50
},
{
"id": 20,
"origin": 5,
"destination": 2,
"tick": 54
},
{
"id": 21,
"origin": 4,
"destination": 3,
"tick": 56
},
{
"id": 22,
"origin": 1,
"destination": 5,
"tick": 57
},
{
"id": 23,
"origin": 4,
"destination": 1,
"tick": 58
},
{
"id": 24,
"origin": 5,
"destination": 1,
"tick": 60
},
{
"id": 25,
"origin": 4,
"destination": 5,
"tick": 62
},
{
"id": 26,
"origin": 5,
"destination": 0,
"tick": 63
},
{
"id": 27,
"origin": 0,
"destination": 2,
"tick": 64
},
{
"id": 28,
"origin": 4,
"destination": 1,
"tick": 65
},
{
"id": 29,
"origin": 2,
"destination": 1,
"tick": 66
},
{
"id": 30,
"origin": 2,
"destination": 4,
"tick": 67
},
{
"id": 31,
"origin": 3,
"destination": 5,
"tick": 69
},
{
"id": 32,
"origin": 2,
"destination": 5,
"tick": 71
},
{
"id": 33,
"origin": 2,
"destination": 0,
"tick": 72
},
{
"id": 34,
"origin": 4,
"destination": 1,
"tick": 73
},
{
"id": 35,
"origin": 3,
"destination": 0,
"tick": 74
},
{
"id": 36,
"origin": 0,
"destination": 3,
"tick": 76
},
{
"id": 37,
"origin": 0,
"destination": 5,
"tick": 77
},
{
"id": 38,
"origin": 3,
"destination": 2,
"tick": 78
},
{
"id": 39,
"origin": 3,
"destination": 1,
"tick": 79
},
{
"id": 40,
"origin": 3,
"destination": 1,
"tick": 87
},
{
"id": 41,
"origin": 3,
"destination": 2,
"tick": 91
},
{
"id": 42,
"origin": 1,
"destination": 3,
"tick": 95
},
{
"id": 43,
"origin": 4,
"destination": 3,
"tick": 96
},
{
"id": 44,
"origin": 3,
"destination": 0,
"tick": 97
},
{
"id": 45,
"origin": 1,
"destination": 4,
"tick": 98
},
{
"id": 46,
"origin": 1,
"destination": 5,
"tick": 102
},
{
"id": 47,
"origin": 5,
"destination": 0,
"tick": 103
},
{
"id": 48,
"origin": 2,
"destination": 5,
"tick": 105
},
{
"id": 49,
"origin": 5,
"destination": 3,
"tick": 106
},
{
"id": 50,
"origin": 0,
"destination": 3,
"tick": 109
},
{
"id": 51,
"origin": 1,
"destination": 5,
"tick": 110
},
{
"id": 52,
"origin": 0,
"destination": 5,
"tick": 111
},
{
"id": 53,
"origin": 3,
"destination": 2,
"tick": 112
},
{
"id": 54,
"origin": 5,
"destination": 1,
"tick": 114
},
{
"id": 55,
"origin": 1,
"destination": 4,
"tick": 116
},
{
"id": 56,
"origin": 1,
"destination": 5,
"tick": 117
},
{
"id": 57,
"origin": 1,
"destination": 4,
"tick": 118
},
{
"id": 58,
"origin": 1,
"destination": 4,
"tick": 119
},
{
"id": 59,
"origin": 0,
"destination": 2,
"tick": 121
},
{
"id": 60,
"origin": 0,
"destination": 4,
"tick": 122
},
{
"id": 61,
"origin": 4,
"destination": 2,
"tick": 123
},
{
"id": 62,
"origin": 0,
"destination": 3,
"tick": 124
},
{
"id": 63,
"origin": 4,
"destination": 3,
"tick": 128
},
{
"id": 64,
"origin": 1,
"destination": 5,
"tick": 129
},
{
"id": 65,
"origin": 5,
"destination": 2,
"tick": 133
},
{
"id": 66,
"origin": 1,
"destination": 4,
"tick": 134
},
{
"id": 67,
"origin": 5,
"destination": 3,
"tick": 136
},
{
"id": 68,
"origin": 1,
"destination": 0,
"tick": 145
},
{
"id": 69,
"origin": 3,
"destination": 2,
"tick": 146
},
{
"id": 70,
"origin": 3,
"destination": 5,
"tick": 148
},
{
"id": 71,
"origin": 4,
"destination": 5,
"tick": 149
},
{
"id": 72,
"origin": 2,
"destination": 5,
"tick": 151
},
{
"id": 73,
"origin": 4,
"destination": 3,
"tick": 155
},
{
"id": 74,
"origin": 5,
"destination": 3,
"tick": 157
},
{
"id": 75,
"origin": 5,
"destination": 0,
"tick": 158
},
{
"id": 76,
"origin": 2,
"destination": 0,
"tick": 162
},
{
"id": 77,
"origin": 4,
"destination": 2,
"tick": 163
},
{
"id": 78,
"origin": 5,
"destination": 3,
"tick": 165
},
{
"id": 79,
"origin": 2,
"destination": 0,
"tick": 166
},
{
"id": 80,
"origin": 2,
"destination": 4,
"tick": 168
},
{
"id": 81,
"origin": 3,
"destination": 1,
"tick": 169
},
{
"id": 82,
"origin": 0,
"destination": 5,
"tick": 173
},
{
"id": 83,
"origin": 4,
"destination": 3,
"tick": 174
},
{
"id": 84,
"origin": 5,
"destination": 4,
"tick": 175
},
{
"id": 85,
"origin": 3,
"destination": 4,
"tick": 177
},
{
"id": 86,
"origin": 0,
"destination": 3,
"tick": 180
},
{
"id": 87,
"origin": 2,
"destination": 5,
"tick": 181
},
{
"id": 88,
"origin": 1,
"destination": 4,
"tick": 185
},
{
"id": 89,
"origin": 5,
"destination": 3,
"tick": 191
},
{
"id": 90,
"origin": 0,
"destination": 1,
"tick": 192
},
{
"id": 91,
"origin": 5,
"destination": 2,
"tick": 193
},
{
"id": 92,
"origin": 3,
"destination": 4,
"tick": 194
},
{
"id": 93,
"origin": 4,
"destination": 2,
"tick": 195
},
{
"id": 94,
"origin": 2,
"destination": 4,
"tick": 196
},
{
"id": 95,
"origin": 3,
"destination": 2,
"tick": 197
},
{
"id": 96,
"origin": 5,
"destination": 3,
"tick": 199
}
]
}

View File

@@ -0,0 +1,482 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "random",
"scale": "medium",
"description": "随机流量 - 均匀分布,适合所有规模 (medium规模)",
"expected_passengers": 78,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 1,
"destination": 2,
"tick": 3
},
{
"id": 2,
"origin": 4,
"destination": 2,
"tick": 5
},
{
"id": 3,
"origin": 5,
"destination": 4,
"tick": 10
},
{
"id": 4,
"origin": 0,
"destination": 3,
"tick": 15
},
{
"id": 5,
"origin": 2,
"destination": 4,
"tick": 18
},
{
"id": 6,
"origin": 4,
"destination": 5,
"tick": 19
},
{
"id": 7,
"origin": 4,
"destination": 2,
"tick": 20
},
{
"id": 8,
"origin": 3,
"destination": 4,
"tick": 25
},
{
"id": 9,
"origin": 0,
"destination": 2,
"tick": 26
},
{
"id": 10,
"origin": 0,
"destination": 4,
"tick": 28
},
{
"id": 11,
"origin": 3,
"destination": 4,
"tick": 36
},
{
"id": 12,
"origin": 4,
"destination": 3,
"tick": 38
},
{
"id": 13,
"origin": 0,
"destination": 4,
"tick": 39
},
{
"id": 14,
"origin": 5,
"destination": 2,
"tick": 40
},
{
"id": 15,
"origin": 1,
"destination": 3,
"tick": 41
},
{
"id": 16,
"origin": 1,
"destination": 5,
"tick": 42
},
{
"id": 17,
"origin": 1,
"destination": 2,
"tick": 46
},
{
"id": 18,
"origin": 0,
"destination": 4,
"tick": 48
},
{
"id": 19,
"origin": 2,
"destination": 5,
"tick": 51
},
{
"id": 20,
"origin": 0,
"destination": 5,
"tick": 53
},
{
"id": 21,
"origin": 3,
"destination": 4,
"tick": 54
},
{
"id": 22,
"origin": 4,
"destination": 3,
"tick": 55
},
{
"id": 23,
"origin": 4,
"destination": 3,
"tick": 61
},
{
"id": 24,
"origin": 0,
"destination": 1,
"tick": 62
},
{
"id": 25,
"origin": 0,
"destination": 4,
"tick": 63
},
{
"id": 26,
"origin": 1,
"destination": 0,
"tick": 70
},
{
"id": 27,
"origin": 0,
"destination": 3,
"tick": 72
},
{
"id": 28,
"origin": 1,
"destination": 2,
"tick": 73
},
{
"id": 29,
"origin": 1,
"destination": 4,
"tick": 74
},
{
"id": 30,
"origin": 5,
"destination": 2,
"tick": 77
},
{
"id": 31,
"origin": 5,
"destination": 4,
"tick": 81
},
{
"id": 32,
"origin": 1,
"destination": 3,
"tick": 83
},
{
"id": 33,
"origin": 5,
"destination": 4,
"tick": 86
},
{
"id": 34,
"origin": 0,
"destination": 2,
"tick": 88
},
{
"id": 35,
"origin": 3,
"destination": 2,
"tick": 89
},
{
"id": 36,
"origin": 1,
"destination": 0,
"tick": 93
},
{
"id": 37,
"origin": 5,
"destination": 0,
"tick": 98
},
{
"id": 38,
"origin": 4,
"destination": 3,
"tick": 104
},
{
"id": 39,
"origin": 2,
"destination": 4,
"tick": 106
},
{
"id": 40,
"origin": 2,
"destination": 1,
"tick": 110
},
{
"id": 41,
"origin": 4,
"destination": 0,
"tick": 117
},
{
"id": 42,
"origin": 3,
"destination": 5,
"tick": 119
},
{
"id": 43,
"origin": 0,
"destination": 1,
"tick": 121
},
{
"id": 44,
"origin": 3,
"destination": 4,
"tick": 122
},
{
"id": 45,
"origin": 2,
"destination": 3,
"tick": 123
},
{
"id": 46,
"origin": 1,
"destination": 0,
"tick": 124
},
{
"id": 47,
"origin": 3,
"destination": 5,
"tick": 125
},
{
"id": 48,
"origin": 1,
"destination": 0,
"tick": 127
},
{
"id": 49,
"origin": 0,
"destination": 2,
"tick": 129
},
{
"id": 50,
"origin": 5,
"destination": 0,
"tick": 133
},
{
"id": 51,
"origin": 2,
"destination": 1,
"tick": 135
},
{
"id": 52,
"origin": 1,
"destination": 0,
"tick": 137
},
{
"id": 53,
"origin": 4,
"destination": 2,
"tick": 138
},
{
"id": 54,
"origin": 1,
"destination": 0,
"tick": 141
},
{
"id": 55,
"origin": 1,
"destination": 3,
"tick": 143
},
{
"id": 56,
"origin": 0,
"destination": 1,
"tick": 145
},
{
"id": 57,
"origin": 2,
"destination": 4,
"tick": 147
},
{
"id": 58,
"origin": 2,
"destination": 5,
"tick": 149
},
{
"id": 59,
"origin": 2,
"destination": 4,
"tick": 151
},
{
"id": 60,
"origin": 5,
"destination": 4,
"tick": 152
},
{
"id": 61,
"origin": 4,
"destination": 3,
"tick": 156
},
{
"id": 62,
"origin": 3,
"destination": 1,
"tick": 158
},
{
"id": 63,
"origin": 4,
"destination": 0,
"tick": 160
},
{
"id": 64,
"origin": 4,
"destination": 2,
"tick": 161
},
{
"id": 65,
"origin": 5,
"destination": 0,
"tick": 164
},
{
"id": 66,
"origin": 0,
"destination": 1,
"tick": 167
},
{
"id": 67,
"origin": 3,
"destination": 2,
"tick": 168
},
{
"id": 68,
"origin": 1,
"destination": 5,
"tick": 170
},
{
"id": 69,
"origin": 4,
"destination": 1,
"tick": 171
},
{
"id": 70,
"origin": 5,
"destination": 1,
"tick": 173
},
{
"id": 71,
"origin": 0,
"destination": 4,
"tick": 177
},
{
"id": 72,
"origin": 2,
"destination": 4,
"tick": 180
},
{
"id": 73,
"origin": 2,
"destination": 0,
"tick": 186
},
{
"id": 74,
"origin": 1,
"destination": 0,
"tick": 188
},
{
"id": 75,
"origin": 0,
"destination": 2,
"tick": 190
},
{
"id": 76,
"origin": 1,
"destination": 4,
"tick": 194
},
{
"id": 77,
"origin": 0,
"destination": 2,
"tick": 195
},
{
"id": 78,
"origin": 3,
"destination": 4,
"tick": 196
}
]
}

View File

@@ -0,0 +1,494 @@
{
"building": {
"floors": 6,
"elevators": 2,
"elevator_capacity": 8,
"scenario": "up_peak",
"scale": "medium",
"description": "上行高峰 - 主要从底层到高层 (medium规模)",
"expected_passengers": 80,
"duration": 200
},
"traffic": [
{
"id": 1,
"origin": 0,
"destination": 5,
"tick": 1
},
{
"id": 2,
"origin": 4,
"destination": 5,
"tick": 2
},
{
"id": 3,
"origin": 0,
"destination": 4,
"tick": 4
},
{
"id": 4,
"origin": 0,
"destination": 3,
"tick": 6
},
{
"id": 5,
"origin": 0,
"destination": 2,
"tick": 9
},
{
"id": 6,
"origin": 0,
"destination": 3,
"tick": 11
},
{
"id": 7,
"origin": 0,
"destination": 4,
"tick": 20
},
{
"id": 8,
"origin": 0,
"destination": 3,
"tick": 21
},
{
"id": 9,
"origin": 0,
"destination": 4,
"tick": 22
},
{
"id": 10,
"origin": 0,
"destination": 4,
"tick": 24
},
{
"id": 11,
"origin": 0,
"destination": 1,
"tick": 25
},
{
"id": 12,
"origin": 4,
"destination": 5,
"tick": 26
},
{
"id": 13,
"origin": 0,
"destination": 2,
"tick": 27
},
{
"id": 14,
"origin": 0,
"destination": 2,
"tick": 28
},
{
"id": 15,
"origin": 4,
"destination": 5,
"tick": 30
},
{
"id": 16,
"origin": 0,
"destination": 1,
"tick": 31
},
{
"id": 17,
"origin": 0,
"destination": 3,
"tick": 33
},
{
"id": 18,
"origin": 0,
"destination": 3,
"tick": 37
},
{
"id": 19,
"origin": 0,
"destination": 2,
"tick": 38
},
{
"id": 20,
"origin": 0,
"destination": 4,
"tick": 42
},
{
"id": 21,
"origin": 0,
"destination": 1,
"tick": 46
},
{
"id": 22,
"origin": 0,
"destination": 3,
"tick": 47
},
{
"id": 23,
"origin": 0,
"destination": 1,
"tick": 51
},
{
"id": 24,
"origin": 1,
"destination": 2,
"tick": 54
},
{
"id": 25,
"origin": 0,
"destination": 1,
"tick": 55
},
{
"id": 26,
"origin": 0,
"destination": 5,
"tick": 56
},
{
"id": 27,
"origin": 0,
"destination": 3,
"tick": 61
},
{
"id": 28,
"origin": 0,
"destination": 4,
"tick": 64
},
{
"id": 29,
"origin": 0,
"destination": 4,
"tick": 65
},
{
"id": 30,
"origin": 0,
"destination": 1,
"tick": 66
},
{
"id": 31,
"origin": 0,
"destination": 4,
"tick": 67
},
{
"id": 32,
"origin": 0,
"destination": 3,
"tick": 68
},
{
"id": 33,
"origin": 0,
"destination": 5,
"tick": 71
},
{
"id": 34,
"origin": 0,
"destination": 1,
"tick": 72
},
{
"id": 35,
"origin": 0,
"destination": 1,
"tick": 73
},
{
"id": 36,
"origin": 0,
"destination": 5,
"tick": 75
},
{
"id": 37,
"origin": 0,
"destination": 2,
"tick": 79
},
{
"id": 38,
"origin": 0,
"destination": 4,
"tick": 80
},
{
"id": 39,
"origin": 0,
"destination": 4,
"tick": 82
},
{
"id": 40,
"origin": 0,
"destination": 4,
"tick": 84
},
{
"id": 41,
"origin": 0,
"destination": 5,
"tick": 85
},
{
"id": 42,
"origin": 0,
"destination": 5,
"tick": 86
},
{
"id": 43,
"origin": 0,
"destination": 5,
"tick": 87
},
{
"id": 44,
"origin": 0,
"destination": 1,
"tick": 90
},
{
"id": 45,
"origin": 0,
"destination": 5,
"tick": 91
},
{
"id": 46,
"origin": 4,
"destination": 5,
"tick": 92
},
{
"id": 47,
"origin": 0,
"destination": 3,
"tick": 95
},
{
"id": 48,
"origin": 0,
"destination": 2,
"tick": 96
},
{
"id": 49,
"origin": 0,
"destination": 1,
"tick": 97
},
{
"id": 50,
"origin": 0,
"destination": 5,
"tick": 99
},
{
"id": 51,
"origin": 0,
"destination": 1,
"tick": 100
},
{
"id": 52,
"origin": 0,
"destination": 2,
"tick": 101
},
{
"id": 53,
"origin": 2,
"destination": 5,
"tick": 103
},
{
"id": 54,
"origin": 0,
"destination": 5,
"tick": 104
},
{
"id": 55,
"origin": 0,
"destination": 2,
"tick": 108
},
{
"id": 56,
"origin": 0,
"destination": 4,
"tick": 109
},
{
"id": 57,
"origin": 0,
"destination": 5,
"tick": 110
},
{
"id": 58,
"origin": 0,
"destination": 2,
"tick": 111
},
{
"id": 59,
"origin": 0,
"destination": 4,
"tick": 113
},
{
"id": 60,
"origin": 0,
"destination": 3,
"tick": 115
},
{
"id": 61,
"origin": 0,
"destination": 1,
"tick": 117
},
{
"id": 62,
"origin": 0,
"destination": 1,
"tick": 118
},
{
"id": 63,
"origin": 0,
"destination": 4,
"tick": 119
},
{
"id": 64,
"origin": 0,
"destination": 5,
"tick": 120
},
{
"id": 65,
"origin": 0,
"destination": 3,
"tick": 121
},
{
"id": 66,
"origin": 0,
"destination": 3,
"tick": 123
},
{
"id": 67,
"origin": 0,
"destination": 5,
"tick": 124
},
{
"id": 68,
"origin": 0,
"destination": 1,
"tick": 127
},
{
"id": 69,
"origin": 4,
"destination": 5,
"tick": 128
},
{
"id": 70,
"origin": 0,
"destination": 1,
"tick": 129
},
{
"id": 71,
"origin": 0,
"destination": 4,
"tick": 130
},
{
"id": 72,
"origin": 0,
"destination": 5,
"tick": 131
},
{
"id": 73,
"origin": 0,
"destination": 3,
"tick": 133
},
{
"id": 74,
"origin": 0,
"destination": 2,
"tick": 134
},
{
"id": 75,
"origin": 0,
"destination": 1,
"tick": 137
},
{
"id": 76,
"origin": 0,
"destination": 4,
"tick": 141
},
{
"id": 77,
"origin": 0,
"destination": 2,
"tick": 142
},
{
"id": 78,
"origin": 0,
"destination": 1,
"tick": 144
},
{
"id": 79,
"origin": 0,
"destination": 2,
"tick": 145
},
{
"id": 80,
"origin": 0,
"destination": 4,
"tick": 146
}
]
}

119
pyproject.toml Normal file
View File

@@ -0,0 +1,119 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "elevator-py"
dynamic = ["version"]
description = "Python implementation of Elevator Saga game with event system"
readme = "README.md"
requires-python = ">=3.10"
license = {file = "LICENSE"}
authors = [
{name = "ZGCA-Forge Team", email = "zgca@zgca.com"}
]
keywords = ["elevator", "simulation", "game", "event-driven", "optimization"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Games/Entertainment :: Simulation",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"numpy>=1.20.0",
"flask>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=22.0.0",
"isort>=5.0.0",
"mypy>=1.0.0",
"flake8>=6.0.0",
"pre-commit>=2.20.0",
"bump2version>=1.0.0",
]
docs = [
"sphinx>=5.0.0",
"sphinx-rtd-theme>=1.0.0",
"myst-parser>=0.18.0",
]
all = [
"elevatorpy[dev,docs]",
]
[project.scripts]
elevator-saga = "elevator_saga.cli.main:main"
elevator-server = "elevator_saga.cli.main:server_main"
elevator-client = "elevator_saga.cli.main:client_main"
elevator-grader = "elevator_saga.grader.grader:main"
elevator-batch-test = "elevator_saga.grader.batch_runner:main"
[project.urls]
Homepage = "https://github.com/ZGCA-Forge/Elevator"
Documentation = "https://zgca-forge.github.io/Elevator/"
Repository = "https://github.com/ZGCA-Forge/Elevator"
Issues = "https://github.com/ZGCA-Forge/Elevator/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["elevator_saga*"]
[tool.setuptools.dynamic]
version = {attr = "elevator_saga.__version__"}
[tool.black]
line-length = 120
target-version = ['py310', 'py311', 'py312']
[tool.isort]
profile = "black"
multi_line_output = 3
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"numpy.*",
"flask.*",
]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "-v --tb=short --strict-markers --strict-config -ra --color=yes"
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]
[tool.bandit]
exclude_dirs = ["tests", "test_*.py", "*_test.py", ".venv", "venv", "build", "dist"]
# B101: assert语句用于类型检查和开发时验证不是安全问题
# B601: shell命令参数化在受控环境中使用
# B310: urllib.urlopen用于连接受控的API服务器URL来源可信
# B311: random模块用于生成电梯流量模拟数据非加密用途
skips = ["B101", "B601", "B310", "B311"]

View File

@@ -1,33 +1,30 @@
{
"include": ["."],
"exclude": [
"build",
"dist",
"__pycache__",
".mypy_cache",
".pytest_cache",
"htmlcov",
".idea",
".vscode",
"docs/_build",
"elevator_saga.egg-info"
],
"pythonVersion": "3.10",
"typeCheckingMode": "strict",
"executionEnvironments": [
{
"root": ".",
"extraPaths": ["elevator_saga"]
}
],
"reportMissingImports": "none",
"reportUnusedImport": "warning",
"reportUnusedVariable": "warning",
"reportUnknownArgumentType": "warning",
"reportUnknownMemberType": "warning",
"reportUnknownVariableType": "warning",
"reportUnknownParameterType": "warning",
"reportPrivateUsage": "warning",
"reportMissingTypeStubs": false
}
"include": ["."],
"exclude": [
"build",
"dist",
"__pycache__",
".mypy_cache",
".pytest_cache",
"htmlcov",
".idea",
".vscode",
"docs/_build",
"elevatorpy.egg-info",
"elevator_saga.egg-info",
".eggs",
"MsgCenterPy"
],
"pythonVersion": "3.10",
"typeCheckingMode": "basic",
"executionEnvironments": [
{
"root": ".",
"extraPaths": ["elevator_saga"]
}
],
"reportMissingImports": "warning",
"reportUnusedImport": "warning",
"reportUnusedVariable": "warning",
"reportMissingTypeStubs": false
}

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env python3
"""
Setup script for Elevator Saga Python Package
"""
from setuptools import setup, find_packages
with open("README_CN.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="elevator-saga",
version="1.0.0",
author="Elevator Saga Team",
description="Python implementation of Elevator Saga game with PyEE event system",
long_description=long_description,
long_description_content_type="text/markdown",
packages=find_packages(),
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Games/Entertainment :: Simulation",
"Topic :: Software Development :: Libraries :: Python Modules",
],
python_requires=">=3.8",
install_requires=[
"pyee>=11.0.0",
"numpy>=1.20.0",
"matplotlib>=3.5.0",
"seaborn>=0.11.0",
"pandas>=1.3.0",,
"flask
],
extras_require={
"dev": [
"pytest>=6.0",
"pytest-cov",
"black",
"flake8",
],
},
entry_points={
"console_scripts": [
"elevator-saga=elevator_saga.cli.main:main",
"elevator-server=elevator_saga.cli.main:server_main",
"elevator-client=elevator_saga.cli.main:client_main",
"elevator-grader=elevator_saga.grader.grader:main",
"elevator-batch-test=elevator_saga.grader.batch_runner:main",
],
},
include_package_data=True,
zip_safe=False,
)

3
tests/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Elevator Saga Test Suite
"""

73
tests/test_imports.py Normal file
View File

@@ -0,0 +1,73 @@
"""
Test module imports to ensure all modules can be imported successfully
"""
import pytest
def test_import_core_models():
"""Test importing core data models"""
from elevator_saga.core.models import (
Direction,
ElevatorState,
ElevatorStatus,
EventType,
FloorState,
PassengerInfo,
PassengerStatus,
SimulationState,
TrafficEntry,
TrafficPattern,
)
assert Direction is not None
assert ElevatorState is not None
assert ElevatorStatus is not None
assert EventType is not None
assert FloorState is not None
assert PassengerInfo is not None
assert PassengerStatus is not None
assert SimulationState is not None
assert TrafficEntry is not None
assert TrafficPattern is not None
def test_import_client_api():
"""Test importing client API"""
from elevator_saga.client.api_client import ElevatorAPIClient
assert ElevatorAPIClient is not None
def test_import_proxy_models():
"""Test importing proxy models"""
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
assert ProxyElevator is not None
assert ProxyFloor is not None
assert ProxyPassenger is not None
def test_import_base_controller():
"""Test importing base controller"""
from elevator_saga.client.base_controller import ElevatorController
assert ElevatorController is not None
def test_import_simulator():
"""Test importing simulator"""
from elevator_saga.server.simulator import ElevatorSimulation
assert ElevatorSimulation is not None
def test_import_client_example():
"""Test importing client example"""
from elevator_saga.client_examples.bus_example import ElevatorBusExampleController
assert ElevatorBusExampleController is not None
if __name__ == "__main__":
pytest.main([__file__, "-v"])