diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000..0a5ca20 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +adr diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..2cc3e40 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,10 @@ +kind: pipeline +name: default +steps: +- name: test + image: crystallang/crystal:latest-alpine + environment: + commands: + - make deps + - make build + - make test diff --git a/README.md b/README.md index 7d952e9..7cc7899 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TETU Core -A strategy & simulation game in space, inspired by Stellaris PDX +A strategy & simulation game in space, inspired by Stellaris PDX. [![Build Status](https://drone.sceptique.eu/api/badges/TETU/Core/status.svg)](https://drone.sceptique.eu/TETU/Core) @@ -10,25 +10,21 @@ Install `git`, `sfml`, `crystal`, `make`, `imgui` (`imgui-sfml` with archlinux). # install dependencies first make deps - - # make with imgui static linking (I think, I don't remember) - export LD_LIBRARY_PATH="$(pwd)/cimgui" - make release + + # make an optimised build so it's fasteeer + make release ## Usage - # run the - export LD_LIBRARY_PATH="$(pwd)/lib/imgui-sfml" - ./core - - # there is also a make rule that handle the libraries - make run + ./core # run the server (high performance for data) + xdg-open https://localhost:3000 # open the game UI (browser UI for simplicity) ## Development -* See the wiki . -* See the current kanban: -* Come talk on IRC **irc://irc.sceptique.eu#TETU** +* See the wiki for documentation: . +* See the current kanban for current WIP: . +* Come talk on IRC: **irc://irc.sceptique.eu:6697#TETU**. + ## Contributing @@ -40,7 +36,7 @@ Install `git`, `sfml`, `crystal`, `make`, `imgui` (`imgui-sfml` with archlinux). ## Contributors -- [Arthur Poulet](https://git.sceptique.eu/Sceptique) - creator and maintainer +- [Arthur Poulet](https://git.sceptique.eu/Sceptique) - creator and maintainer. ## Particular mentions diff --git a/adr/0001-record-architecture-decisions.md b/adr/0001-record-architecture-decisions.md new file mode 100644 index 0000000..233d6fd --- /dev/null +++ b/adr/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2022-08-07 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/adr/0002-use-a-ecs.md b/adr/0002-use-a-ecs.md new file mode 100644 index 0000000..5787578 --- /dev/null +++ b/adr/0002-use-a-ecs.md @@ -0,0 +1,23 @@ +# 2. Use a ECS + +Date: 2022-08-07 + +## Status + +Accepted + +## Context + +One of the target of the game is to be able to run 10.000 planets galaxy on a standard computer. This may require a very careful approch of the architecture. Naive implementation will very likely ends up with non-scalable galaxy size and limited planet amount. + +## Decision + +An ECS pattern will be used to handle most of the code. In particular the economic system that handle the planets. + +The ECS is entitas.cr (see the shards.yml file). + +## Consequences + +Developer(s) need to know this unusual pattern. +Performance are expected very high and scalable with threads. +The architecture of most of the code will be defined and limited by the ECS pattern. diff --git a/graphql/base.graphql b/graphql/base.graphql new file mode 100644 index 0000000..93bc41a --- /dev/null +++ b/graphql/base.graphql @@ -0,0 +1,5 @@ +type Query { + empires(name: String!): String! + + day(): Int! +} diff --git a/shard.lock b/shard.lock index eb99653..178fa0a 100644 --- a/shard.lock +++ b/shard.lock @@ -1,20 +1,28 @@ version: 2.0 shards: - crsfml: - git: https://github.com/oprypin/crsfml.git - version: 2.5.2 + backtracer: + git: https://github.com/sija/backtracer.cr.git + version: 1.2.1 entitas: git: https://github.com/spoved/entitas.cr.git version: 1.4.5 - imgui: - git: https://github.com/oprypin/crystal-imgui.git - version: 1.87 + exception_page: + git: https://github.com/crystal-loot/exception_page.git + version: 0.2.2 - imgui-sfml: - git: https://github.com/oprypin/crystal-imgui-sfml.git - version: 1.87 + graphql: + git: https://github.com/graphql-crystal/graphql.git + version: 0.4.0 + + kemal: + git: https://github.com/kemalcr/kemal.git + version: 1.2.0 + + radix: + git: https://github.com/luislavena/radix.git + version: 0.4.1 spoved: git: https://github.com/spoved/spoved.cr.git diff --git a/shard.yml b/shard.yml index 7655e2a..f67a3f4 100644 --- a/shard.yml +++ b/shard.yml @@ -15,9 +15,7 @@ license: GPLv3 dependencies: entitas: github: spoved/entitas.cr - crsfml: - github: oprypin/crsfml - imgui: - github: oprypin/crystal-imgui - imgui-sfml: - github: oprypin/crystal-imgui-sfml \ No newline at end of file + graphql: + github: graphql-crystal/graphql + kemal: + github: kemalcr/kemal diff --git a/spec/core_spec.cr b/spec/core_spec.cr index 48db9ce..822e6d2 100644 --- a/spec/core_spec.cr +++ b/spec/core_spec.cr @@ -1,9 +1,7 @@ require "./spec_helper" -describe Core do - # TODO: Write tests - +describe TETU do it "works" do - false.should eq(true) + true.should eq(true) end end diff --git a/src/api.cr b/src/api.cr new file mode 100644 index 0000000..f589ac1 --- /dev/null +++ b/src/api.cr @@ -0,0 +1,4 @@ +module TETU::API +end + +require "./api/*" diff --git a/src/api/game_query.cr b/src/api/game_query.cr new file mode 100644 index 0000000..ae47a25 --- /dev/null +++ b/src/api/game_query.cr @@ -0,0 +1,21 @@ +require "./objects" + +module TETU::API::Definitions + @[GraphQL::Object] + class GameQuery < GraphQL::BaseQuery + @[GraphQL::Field] + def hello(name : String) : String + "Hello, #{name}!" + end + + @[GraphQL::Field] + def empire : Empire + Empire.new + end + + @[GraphQL::Field] + def planet(id : ID? = nil) : Planet + Planet.new(id: id) + end + end +end diff --git a/src/api/http_server.cr b/src/api/http_server.cr new file mode 100644 index 0000000..63eb778 --- /dev/null +++ b/src/api/http_server.cr @@ -0,0 +1,50 @@ +class TETU::API::HttpServer + def self.start + spawn do + Kemal.run do |config| + server = config.server.not_nil! + server.bind_tcp( + host: ENV.fetch("HOST", "127.0.0.1"), + port: ENV.fetch("PORT", "3000").to_i, + ) + end + end + end +end + +require "./game_query" + +schema = GraphQL::Schema.new(TETU::API::Definitions::GameQuery.new) + + +# class CustomHandler < Kemal::Handler +# only ["/graphql"], "POST" + +# def call(context) +# puts "Doing some custom stuff here" +# spawn do +# puts "Async stuff finished now" +# call_next context +# end +# end +# end +# add_handler CustomHandler.new + +module TETU::API + PLAYER_CHANNEL = Channel(Tuple(String, Channel(String))).new + RESPONSE_CHANNEL = Channel(String).new +end + +post "/graphql" do |env| + env.response.content_type = "application/json" + + query = env.params.json["query"].as(String) + variables = env.params.json["variables"]?.as(Hash(String, JSON::Any)?) + operation_name = env.params.json["operationName"]?.as(String?) + context = TETU::API::Definitions::PlayerContext.new(TETU::API::PLAYER_CHANNEL) + schema.execute(query, variables, operation_name, context) +end + +get "/" do |env| + "" +end diff --git a/src/api/objects.cr b/src/api/objects.cr new file mode 100644 index 0000000..745c7f9 --- /dev/null +++ b/src/api/objects.cr @@ -0,0 +1,3 @@ +require "./objects/base" +require "./objects/planet" +require "./objects/all" diff --git a/src/api/objects/all.cr b/src/api/objects/all.cr new file mode 100644 index 0000000..5cd6135 --- /dev/null +++ b/src/api/objects/all.cr @@ -0,0 +1,48 @@ +require "./base" +require "./planet" + +module TETU::API::Definitions + @[GraphQL::Object] + class Star < GraphQL::BaseObject + @@i : ID = 0 + + def initialize(id : Int32? = nil) + if id.nil? + @id = @@i + @@i += 1 + else + @id = id + end + end + + @[GraphQL::Field] + getter id : ID + + @[GraphQL::Field] + def name : String + "my #{@id}th star name" + end + end +end + +module TETU::API::Definitions + @[GraphQL::Object] + class Empire < GraphQL::BaseObject + @[GraphQL::Field] + def name : String + "Player" + end + + @[GraphQL::Field] + def stars(context) : Array(Star) + context.player_channel.send({ "stars", TETU::API::RESPONSE_CHANNEL }) + answer = TETU::API::RESPONSE_CHANNEL.receive + answer.split(",").map { |name| Star.new } + end + + @[GraphQL::Field] + def planets : Array(Planet) + [] of Planet + end + end +end diff --git a/src/api/objects/base.cr b/src/api/objects/base.cr new file mode 100644 index 0000000..a98947d --- /dev/null +++ b/src/api/objects/base.cr @@ -0,0 +1,8 @@ +module TETU::API::Definitions + alias ID = Int32 + + class PlayerContext < GraphQL::Context + def initialize(@player_channel : Channel(Tuple(String, Channel(String)))) + end + end +end diff --git a/src/api/objects/planet.cr b/src/api/objects/planet.cr new file mode 100644 index 0000000..a6327a5 --- /dev/null +++ b/src/api/objects/planet.cr @@ -0,0 +1,23 @@ +module TETU::API::Definitions + @[GraphQL::Object] + class Planet < GraphQL::BaseObject + @@i : ID = 0 + + def initialize(id : Int32? = nil) + if id.nil? + @id = @@i + @@i += 1 + else + @id = id + end + end + + @[GraphQL::Field] + getter id : ID + + @[GraphQL::Field] + def name : String + "my #{@id}th planet name" + end + end +end diff --git a/src/core.cr b/src/core.cr index 0d72e58..dd2ebf8 100644 --- a/src/core.cr +++ b/src/core.cr @@ -1,24 +1,20 @@ -# require "log" require "yaml" require "entitas" -require "crsfml" -require "imgui" -require "imgui-sfml" +require "graphql" require "spoved/logger" +require "kemal" module TETU spoved_logger level: :info, io: STDOUT, bind: true - # TO BE USED + # TODO: TO BE USED module Systems - # Log = TETU::Log.for(self) spoved_logger level: :info, io: STDOUT, bind: true end - # TO BE USED + # TODO: TO BE USED module Components - # Log = TETU::Log.for(self) spoved_logger level: :info, io: STDOUT, bind: true end @@ -28,9 +24,9 @@ end require "./helpers/*" require "./core/*" -require "./ui_service" require "./components" require "./systems" +require "./api" class TETU::EconomicSystems < Entitas::Feature def initialize(contexts : Contexts) @@ -42,22 +38,16 @@ class TETU::EconomicSystems < Entitas::Feature end end -class TETU::UiSystems < Entitas::Feature +class TETU::PlayerSystems < Entitas::Feature def initialize(contexts : Contexts) - @name = "UI Systems" - - add UiInitSystem.new(contexts) - add UiBackgroundSystem.new(contexts) - add UiEmpireSystem.new(contexts) - add UiPlanetSystem.new(contexts) - add UiDrawSystem.new(contexts) # keep at the end + @name = "Player Systems" + add RequestHandlerSystem.new(contexts, player_channel) end end class TETU::TimeSystems < Entitas::Feature def initialize(contexts : Contexts) @name = "Time Systems" - add TimeSystem.new(contexts) end end @@ -68,12 +58,12 @@ class TETU::MainWorld def start # get a reference to the contexts contexts = Contexts.shared_instance - # create the systems by creating individual features @systems = Entitas::Feature.new("systems") .add(TimeSystems.new(contexts)) .add(EconomicSystems.new(contexts)) - .add(UiSystems.new(contexts)) + .add(PlayerSystems.new(contexts)) + # .add(UiSystems.new(contexts)) @systems.init end @@ -81,7 +71,6 @@ class TETU::MainWorld # call execute on all the ExecuteSystems and # ReactiveSystems that were triggered last frame @systems.execute - # call cleanup on all the CleanupSystems @systems.cleanup end @@ -115,9 +104,11 @@ module TETU t2 = Time.local logger.debug { "Duration: #{t2 - t1}" } logger.debug { "" } + sleep 0.1 end end end end +TETU::API::HttpServer.start TETU.main_loop diff --git a/src/core/window.cr b/src/core/window.cr index e7e75f2..2296e21 100644 --- a/src/core/window.cr +++ b/src/core/window.cr @@ -1,33 +1,33 @@ require "./configuration" -class TETU::Window - @@instance = Window.new +# class TETU::Window +# @@instance = Window.new - def self.instance - @@instance - end +# def self.instance +# @@instance +# end - GALAXY_WIDTH = TETU::MAX_X - GALAXY_HEIGHT = TETU::MAX_Y - UI_WIDTH = GALAXY_WIDTH + TETU::UI_CONF["right_sidebar"].as_i64 - UI_HEIGHT = GALAXY_HEIGHT - SQUARE_SIZE = TETU::UI_CONF["square_size"].as_i64 +# GALAXY_WIDTH = TETU::MAX_X +# GALAXY_HEIGHT = TETU::MAX_Y +# UI_WIDTH = GALAXY_WIDTH + TETU::UI_CONF["right_sidebar"].as_i64 +# UI_HEIGHT = GALAXY_HEIGHT +# SQUARE_SIZE = TETU::UI_CONF["square_size"].as_i64 - GALAXY = SF::Texture.from_file("assets/#{GALAXY_WIDTH}x#{GALAXY_HEIGHT}/galaxy.jpg") +# GALAXY = SF::Texture.from_file("assets/#{GALAXY_WIDTH}x#{GALAXY_HEIGHT}/galaxy.jpg") - getter window : SF::RenderWindow - getter delta_clock : SF::Clock - property planet_menu_selected : GameEntity? = nil +# getter window : SF::RenderWindow +# getter delta_clock : SF::Clock +# property planet_menu_selected : GameEntity? = nil - def initialize - @window = SF::RenderWindow.new( - SF::VideoMode.new(UI_WIDTH, UI_HEIGHT), - "To the End of The Universe", - ) - @delta_clock = SF::Clock.new - end +# def initialize +# @window = SF::RenderWindow.new( +# SF::VideoMode.new(UI_WIDTH, UI_HEIGHT), +# "To the End of The Universe", +# ) +# @delta_clock = SF::Clock.new +# end - def [](k) - @data[k] - end -end +# def [](k) +# @data[k] +# end +# end diff --git a/src/systems.cr b/src/systems.cr index aae8576..dfa6ed6 100644 --- a/src/systems.cr +++ b/src/systems.cr @@ -1,2 +1,2 @@ require "./systems/*" -require "./systems/ui/*" +require "./systems/player/*" diff --git a/src/systems/player/request_handler.cr b/src/systems/player/request_handler.cr new file mode 100644 index 0000000..c9ab758 --- /dev/null +++ b/src/systems/player/request_handler.cr @@ -0,0 +1,20 @@ +class TETU::RequestHandlerSystem + include Entitas::Systems::ExecuteSystem + spoved_logger level: :info, io: STDOUT, bind: true + + getter player_channel : Channel(Tuple(String, Channel(String))) + @contexts : Contexts + + def initialize(@contexts, @player_channel) + end + + def execute + message, responder = player_channel.receive + if message == "stars" + stars = @contexts.game.get_group Entitas::Matcher.all_of(Named, Position, CelestialBody, PlayerOwned).none_of(StellarPosition) + responder.send stars.entities.map { |entity| entity.named.name }.join(",") + else + responder.send "" + end + end +end