velkuns/game-text-engine 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

velkuns/game-text-engine

最新稳定版本:0.5.0

Composer 安装命令:

composer require velkuns/game-text-engine

包简介

This is a PHP Game Text Engine to create games based on texts, with choices, items, and more.

README 文档

README

Current version Supported PHP version CI Quality Gate Status Coverage

Why?

This is a PHP Game Text Engine to create games based on texts, with choices, items, and more.

Installation

If you wish to install it in your project, require it via composer:

composer require velkuns/game-text-engine

API Documentation

Game Object

declare(strict_types=1);

namespace Application;

use Velkuns\GameTextEngine\Api\ItemsApi;
use Velkuns\GameTextEngine\Api\PlayerApi;
use Velunns\GameTextEngine\Api\GameApi;
use Velkuns\GameTextEngine\Core\Factory\AttributeFactory;
use Velkuns\GameTextEngine\Core\Loader\JsonLoader;

//~ Factories
$modifierFactory  = new ModifierFactory();
$itemFactory      = new ItemFactory($modifierFactory);
$conditionFactory = new ConditionsFactory(new ConditionParser(), new ConditionElementResolver(), new ConditionValidator());
$graphFactory     = new GraphFactory($conditionFactory);
$attributeFactory = new AttributeFactory();
$entityFactory    = new EntityFactory(
    $attributeFactory, 
    new TraitFactory($modifierFactory, $conditionFactory), 
    $itemFactory
);

$items   = new ItemsApi($itemFactory);
$gameApi = new GameApi(
    new JsonLoader(),
    new StoryApi($graphFactory),
    $items,
    new BestiaryApi($entityFactory, $items),
    new AttributesApi($attributeFactory),
    new PlayerApi($entityFactory, $items),
    new CombatApi(new Randomizer(new Mt19937())),
);

//~ Load json data (can be from files or strings if came from database)
$storyData            = $game->loader->fromFile($dataDir . '/stories/test.json');
$itemsData            = $game->loader->fromFile($dataDir . '/items.json');
$bestiaryData         = $game->loader->fromFile($dataDir . '/bestiary.json');
$attributesRulesData  = $game->loader->fromFile($dataDir . '/rules/rules_attributes.json');
$traitsRulesData      = $game->loader->fromFile($dataDir . '/rules/rules_traits.json');
$alterationsRulesData = $game->loader->fromFile($dataDir . '/rules/rules_alterations.json');
$combatsRulesData     = $game->loader->fromFile($dataDir . '/rules/rules_combat.json');
$playerRulesData      = $game->loader->fromFile($dataDir . '/rules/rules_player.json');
$playerData           = $game->loader->fromFile($dataDir . '/templates/player.json');

//~ Load data into the game api
$gameApi->load(
    $storyData, 
    $itemsData, 
    $bestiaryData, 
    $traitsRulesData,
    $alterationsRulesData,
    $combatsRulesData, 
    $playerRulesData, 
    $playerData
);

//~ Access to the other apis
$gameApi->storyApi->[...];
$gameApi->bestiaryApi->[...];
$gameApi->itemsApi->[...];
$gameApi->attributesApi->[...];
$gameApi->traitsApi->[...];
$gameApi->alterationssApi->[...];
$gameApi->playerApi->[...];

//~ Dumping apis into json data

/**
 * @phpstan-return array{
 *     story: string,
 *     items: string,
 *     bestiary: string,
 *     attributesRules: string,
 *     traitsRules: string,
 *     alterationsRules: string,
 *     combatRules: string,
 *     playerRules: string,
 *     playerData: string,
 * } $data Array of json data, to save in files or database
 */
$data = $gameApi->dump(/* true */); // true to pretty json output

//~ Exporting story graph into DOT data
$gameApi->exporter->toFile($gameApi->storyApi->graph, [...]);      // export story graph to file
$string = $gameApi->exporter->toString($gameApi->storyApi->graph); // export story graph to string

//~ Game API read
$source = '1';
$target = '2';
$gameApi->read($source, $target);

Loader (to load data from files / strings):

<?php

declare(strict_types=1);

namespace Application;

use Velkuns\GameTextEngine\Util\Loader\JsonLoader;

$loader  = new JsonLoader();

//~ To load data from a file
$data = $loader->fromFile('/path/to/data.json');

//~ To load data from a string (JSON format, can be stored in database)
$data = $loader->fromString('{"key": "value"}');

Items API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

//~ Get an item by its name
$item = $game->items->get('Rusty Sword');

$staff = $itemFactory->from(['name' => 'Staff', ...]);
$gameApi->items->set($staff); // Adds or replaces the item in the items dictionary
$gameApi->items->remove($staff->getName()); // Removes the item from the items dictionary

Bestiary API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

//~ Get a creature by its name
$entity = $gameApi->bestiary->get('Goblin');

$goblinWarrior = $entityFactory->from(['name' => 'Goblin Warrior', ...]);
$gameApi->bestiary->set($goblinWarrior); // Adds or replaces the creature in the bestiary
$gameApi->bestiary->remove('Goblin'); // Removes the creature from the bestiary

Attributes API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

//~ Return all attributes: array{simples: array<string, AttributeInterface>, compounds: array<string, AttributeInterface>}
$attributes = $gameApi->attributes->getAll();

$attribute = $gameApi->attributes->get('strength'); // Get one attribute (cloned)
$gameApi->attributes->set($attribute); // Set an attribute
$gameApi->attributes->remove('strength');

Traits API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

//~ Return all traits: array<string, array<string, TraitInterface>>
$traits = $gameApi->traits->getAll();

$trait = $gameApi->traits->get('skill', 'Goblin Hunter'); // Get one trait (cloned)
$gameApi->traits->set($trait); // Set a trait
$gameApi->traits->remove('skill', 'Goblin Hunter');

Alterations API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

//~ Return all alterations: array<string, array<string, TraitInterface>>
$alterations = $gameApi->alterations->getAll();

$alteration = $gameApi->alterations->get('state', 'poisoned'); // Get one alteration (cloned)
$gameApi->alterations->set($alteration); // Set an alteration
$gameApi->alterations->remove('state', 'poisoned');

Story API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here, including player ... ]

//~ Start the story - retrieve the first node of the story
$sourceNodeId = '0';
$targetNodeId = '1';
[$text, $choices, $logs] = $gameApi->story->read($sourceNodeId, $targetNodeId);

Player API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

//~ Create a new player based on the given data
$data = [
    'name'        => 'New Hero',
    'age'         => 25, // optional, default 20
    'race'        => 'elf', // optional, default 'human'
    'description' => 'A brave adventurer.', // optional, default ''
    'background'  => 'Born in a small village.', // optional, default ''
    'attributes'   => [,
        'strength'  => 10,
        'endurance' => 12,
        'agility'   => 14,
        'intuition' => 13,
    ], 
    'inventory' => ['Rusty Sword'], // optional, default []
];

$gameApi->player->new($data);

//~ Get player object
$player = $gameApi->player->player;

Combat API

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

$enemies = [
    $gameApi->bestiary->get('Rat'), // get clone
    $gameApi->bestiary->get('Rat'), // get clone
];

$logs = $gameApi->combat->auto($gameApi->player->player, $enemies);

//~ Display combat results
// ... your code to display combat turns ...

Access to the Story's graph

<?php

declare(strict_types=1);

namespace Application;

// [... game api init code here ... ]

$graph = $gameApi->story->graph;

//~ Manipulate graph nodes
$graph->addNode(new Node(...));
$graph->removeNode('node_id');

//~ Manipulate graph edges (between nodes).
$graph->addEdge(new Edge(...));
$graph->removeEdgeBetweenNodes('node_id_source', 'node_id_target');

Important

When add edges, source node and target node must have already been added to the graph.

Symfony Integration

Yaml config

parameters:
  # Path to initial game data
  game.data.dir:      '../../vendor/velkuns/game-text-engine/data'

  # Files to load when create new story. Should be stored in database after.
  game.data.story:    '%game.data.dir%/stories/test.json'
  game.data.bestiary: '%game.data.dir%/bestiary.json'
  game.data.items:    '%game.data.dir%/items.json'
  game.data.player:   '%game.data.dir%/templates/player.json'

  # Rules files to load when create new story. Should be stored in database after.
  game.data.rules.attributes:  '%game.data.dir%/rules/rules_attributes.json'
  game.data.rules.traits:      '%game.data.dir%/rules/rules_traits.json'
  game.data.rules.alterations: '%game.data.dir%/rules/rules_alterations.json'
  game.data.rules.combat:      '%game.data.dir%/rules/rules_combat.json'
  game.data.rules.player:      '%game.data.dir%/rules/rules_player.json'

services:
  _defaults:
    autowire: true
    bind:
      $typeResolvers:
        - '@Velkuns\GameTextEngine\Core\Resolver\AttributeResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\EntityDamagesResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\EntityInfoResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\EntityInventoryItemsResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\TraitResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\AlterationResolver'

      $valueResolvers:
        - '@Velkuns\GameTextEngine\Core\Resolver\AttributeResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\EntityDamagesResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\EntityInfoResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\EquippedWeaponItemResolver'
        - '@Velkuns\GameTextEngine\Core\Resolver\RollResolver'

      $validators:
        - '@Velkuns\GameTextEngine\Core\Validator\AttributeConditionValidator'
        - '@Velkuns\GameTextEngine\Core\Validator\EntityInfoConditionValidator'
        - '@Velkuns\GameTextEngine\Core\Validator\EntityInventoryItemsConditionValidator'
        - '@Velkuns\GameTextEngine\Core\Validator\TraitConditionValidator'
        - '@Velkuns\GameTextEngine\Core\Validator\AlterationConditionValidator'


  #~ Game text engine source
  Velkuns\GameTextEngine\:
    resource: '../../vendor/velkuns/game-text-engine/src'

  #~ Randomizer for random game part like combat
  Random\Engine\Mt19937: ~
  Random\Randomizer: ~

Example of bridge service between app & Game Text Engine

<?php

/*
 * Copyright (c) velkuns
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Application\Domain\Book\Service;

use Application\Domain\Book\Entity\BookInteractiveGame;
use Application\Domain\Book\Repository\BookInteractiveGameRepositoryInterface;
use Application\Domain\Book\Repository\BookInteractiveRepositoryInterface;
use Velkuns\GameTextEngine\Api\GameApi;
use Velkuns\GameTextEngine\Api\PlayerApi;
use Velkuns\GameTextEngine\Core\Log\LootLog;
use Velkuns\GameTextEngine\Core\Log\XpLog;
use Velkuns\GameTextEngine\Graph\Edge;
use Velkuns\GameTextEngine\Graph\Node;
use Velkuns\GameTextEngine\Core\Log\CombatLog;

/**
 * @phpstan-import-type NewPlayerData from PlayerApi
 */
readonly class StoryPlay
{
    public function __construct(
        public GameApi $api,
        private BookInteractiveRepositoryInterface $bookInteractiveRepository,
        private BookInteractiveGameRepositoryInterface $bookInteractiveGameRepository,
    ) {}

    /**
     * Load a game from database and load content in GameApi to set up the Game Text Engine  
     */
    public function load(int $gameId): BookInteractiveGame
    {
        $bookGame        = $this->bookInteractiveGameRepository->findByIdWithJoined($gameId, ['BookInteractive']);
        $bookInteractive = $bookGame->getBookInteractive();

        //~ Load json data into GameApi
        $this->api->loadFromJsons(
            $bookInteractive->getStory(),          // json string
            $bookInteractive->getItems(),          // json string
            $bookInteractive->getBestiary(),       // json string
            $bookInteractive->getRulesAttributes(), // json string
            $bookInteractive->getRulesTraits(),  // json string
            $bookInteractive->getRulesCombat(),    // json string
            $bookInteractive->getRulesPlayer(),    // json string
            $bookGame->getCharacter(),
        );

        return $bookGame;
    }

    /**
     * Pre-load the Game Text Engine with rules, bestiary & items data, but without player info  
     */
    public function preload(int $bookInteractiveId): void
    {
        $bookInteractive = $this->bookInteractiveRepository->findById($bookInteractiveId);

        //~ Load json data into GameApi
        $this->api->loadFromJsons(
            $bookInteractive->getStory(),          // json string
            $bookInteractive->getItems(),          // json string
            $bookInteractive->getBestiary(),       // json string
            $bookInteractive->getRulesAttributes(), // json string
            $bookInteractive->getRulesTraits(),  // json string
            $bookInteractive->getRulesCombat(),    // json string
            $bookInteractive->getRulesPlayer(),    // json string
        );
    }

    /**
     * Start a new game. Initialize new player with given basic info, and save game state in database.
     * @phpstan-param NewPlayerData $characterData
     */
    public function start(int $bookInteractiveId, int $userId, array $characterData): BookInteractiveGame
    {
        $this->preload($bookInteractiveId);

        //~ Init new player from given character data
        $this->api->player->new($characterData);
        //$this->api->player->player->getInventory()->get('Rusty Sword')

        $game = $this->bookInteractiveGameRepository->newEntity();

        $game->setBookInteractiveId($bookInteractiveId);
        $game->setUserId($userId);

        return $this->save($game, 1, 0);
    }

    /**
     * Read target node, and if not a page refresh, save new state
     * @return array{
     *     0: Node,
     *     1: Edge[],
     *     2: array{combat: array<int, array{player: CombatLog, enemy?: CombatLog}>, loot: list<LootLog>, xp: list<XpLog>}
     * }
     */
    public function read(BookInteractiveGame $bookGame, int $targetId): array
    {
        $source = (string) $bookGame->getTextTargetId(); // Previous target become source
        $target = (string) $targetId;

        $result = $this->api->read($source, $target);

        if ($source !== $target) {
            $this->save($bookGame, $targetId);
        }

        return $result;
    }

    /**
     * Delete game from database 
     */
    public function delete(int $gameId): void
    {
        //~ Set data into entity
        $bookGame = $this->bookInteractiveGameRepository->findById($gameId);
        $this->bookInteractiveGameRepository->delete($bookGame);
    }

    /**
     * Save game into database. Overwrite the current saved state. 
     */
    public function save(BookInteractiveGame $game, int $textTargetId, ?int $textSourceId = null): BookInteractiveGame
    {
        //~ Set data into entity
        $character = $this->api->player->dumpPlayer();

        $game->setCharacter($character);
        $game->setTextSourceId($textSourceId ?? $game->getTextTargetId());
        $game->setTextTargetId($textTargetId);

        $this->bookInteractiveGameRepository->persist($game);

        return $game;
    }
}

Example of database to handle "play book"

---
Tables diagram: nodes (v1)
---
classDiagram
    
    class book_interactive {
        int book_interactive_id
        int book_id
        string book_interactive_story
        string book_interactive_items
        string book_interactive_bestiary
        string book_interactive_rules_attributes
        string book_interactive_rules_traits
        string book_interactive_rules_combat
        string book_interactive_rules_player
    }
    
    class book_interactive_game {
        int game_id
        int book_interactive_id
        int user_id
        string game_character
        int game_text_source_id
        int game_text_target_id
    }
Loading

Contributing

See the CONTRIBUTING file.

Install / update project

You can install project with the following command:

make install

And update with the following command:

make update

NB: For the components, the composer.lock file is not committed.

Testing & CI (Continuous Integration)

Tests

You can run unit tests (with coverage) on your side with following command:

make php/tests

You can run integration tests (without coverage) on your side with following command:

make php/integration

For prettier output (but without coverage), you can use the following command:

make php/testdox # run tests without coverage reports but with prettified output

Code Style

You also can run code style check with following commands:

make php/cs-check

You also can run code style fixes with following commands:

make php/cs-fix

Check for missing explicit dependencies

You can check if any explicit dependency is missing with the following command:

make php/deps

Static Analysis

To perform a static analyze of your code (with phpstan, lvl 9 at default), you can use the following command:

make php/analyse

CI Simulation

And the last "helper" commands, you can run before commit and push, is:

make ci  

License

This project is currently under The MIT License (MIT). See LICENCE file for more information.

统计信息

  • 总下载量: 14
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 2
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 2
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-10-29