Skip to content

Commit

Permalink
Merge pull request #18 from chickensoft-games/feat/less-generics
Browse files Browse the repository at this point in the history
feat: reduce generics
  • Loading branch information
jolexxa authored Oct 7, 2023
2 parents a2d34a4 + 895ef5c commit be85467
Show file tree
Hide file tree
Showing 54 changed files with 1,291 additions and 1,072 deletions.
12 changes: 6 additions & 6 deletions Chickensoft.LogicBlocks.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static class Program {
[ItemType.Candy] = 6
};

public const char EMPTY = '*';
public const char EMPTY = '_';

public static readonly Dictionary<ItemType, char> Chars = new() {
[ItemType.Juice] = 'J',
Expand All @@ -50,15 +50,15 @@ public static class Program {

// Keep a buffer of the last 3 outputs to show them on the screen.
private const int MAX_OUTPUTS = 3;
private static readonly Queue<VendingMachine.Output> _lastFewOutputs =
private static readonly Queue<object> _lastFewOutputs =
new(MAX_OUTPUTS);

public static int Main(string[] args) {
var machine = new VendingMachine(Stock);
var shouldContinue = true;
var lastState = machine.Value;
// Outputs that need to be processed.
var outputs = new Queue<VendingMachine.Output>();
var outputs = new Queue<object>();

Console.CancelKeyPress += (_, _) => shouldContinue = false;
machine.OnOutput += (output) => {
Expand Down Expand Up @@ -241,7 +241,7 @@ private static void ShowOverview() {
Console.WriteLine("Press `q` or `escape` to quit.");
}

private static void ShowOutputs(Queue<VendingMachine.Output> outputs) {
private static void ShowOutputs(Queue<object> outputs) {
if (outputs.Count == 0) { return; }
Console.WriteLine(" -- Last 3 Outputs (Most Recent -> Oldest) --");
var i = 1;
Expand All @@ -250,7 +250,7 @@ private static void ShowOutputs(Queue<VendingMachine.Output> outputs) {
}
}

private static void ProcessOutput(VendingMachine.Output output) {
private static void ProcessOutput(object output) {
if (output is VendingMachine.Output.BeginVending) {
_vendingStartedTime = GetMs();
_isTransactionUnderway = false;
Expand Down Expand Up @@ -332,7 +332,7 @@ private static string ReplaceNTimes(string text, char a, char b, int n) {
return result;
}

private static void AddOutputToBuffer(VendingMachine.Output output) {
private static void AddOutputToBuffer(object output) {
_lastFewOutputs.Enqueue(output);
if (_lastFewOutputs.Count > MAX_OUTPUTS) {
_lastFewOutputs.Dequeue();
Expand Down
33 changes: 15 additions & 18 deletions Chickensoft.LogicBlocks.Example/VendingMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ namespace Chickensoft.LogicBlocks.Example;
public partial class VendingMachine {
// Inputs

public abstract record Input {
public record SelectionEntered(ItemType Type) : Input;
public record PaymentReceived(int Amount) : Input;
public record TransactionTimedOut : Input;
public record VendingCompleted : Input;
public static class Input {
public readonly record struct SelectionEntered(ItemType Type);
public readonly record struct PaymentReceived(int Amount);
public readonly record struct TransactionTimedOut;
public readonly record struct VendingCompleted;
}

public abstract record State(IContext Context) : StateLogic(Context) {
Expand Down Expand Up @@ -140,16 +140,16 @@ public State On(Input.VendingCompleted input) =>

// Side effects

public abstract record Output {
public record Dispensed(ItemType Type) : Output;
public record TransactionStarted : Output;
public record TransactionCompleted(
public static class Output {
public readonly record struct Dispensed(ItemType Type);
public readonly record struct TransactionStarted;
public readonly record struct TransactionCompleted(
ItemType Type, int Price, TransactionStatus Status, int AmountPaid
) : Output;
public record RestartTransactionTimeOutTimer : Output;
public record ClearTransactionTimeOutTimer : Output;
public record MakeChange(int Amount) : Output;
public record BeginVending : Output { }
);
public readonly record struct RestartTransactionTimeOutTimer;
public readonly record struct ClearTransactionTimeOutTimer;
public readonly record struct MakeChange(int Amount);
public readonly record struct BeginVending { }
}

// Feature-specific stuff
Expand All @@ -163,10 +163,7 @@ public record BeginVending : Output { }

// Logic Block / Hierarchical State Machine

public partial class VendingMachine :
LogicBlock<
VendingMachine.Input, VendingMachine.State, VendingMachine.Output
> {
public partial class VendingMachine : LogicBlock<VendingMachine.State> {
public VendingMachine(VendingMachineStock stock) {
Set(stock);
}
Expand Down
150 changes: 77 additions & 73 deletions Chickensoft.LogicBlocks.Generator.Tests/test_cases/Heater.cs
Original file line number Diff line number Diff line change
@@ -1,118 +1,122 @@
namespace Chickensoft.LogicBlocks.Generator.Tests;

using Shouldly;

/// <summary>
/// Temperature sensor that presumably communicates with actual hardware
/// (not shown here).
/// </summary>
public interface ITemperatureSensor {
/// <summary>Last recorded air temperature.</summary>
double AirTemp { get; }
/// <summary>Invoked whenever a change in temperature is noticed.</summary>
event Action<double>? OnTemperatureChanged;
}

public record TemperatureSensor : ITemperatureSensor {
public double AirTemp { get; set; } = 72.0d;
public event Action<double>? OnTemperatureChanged;

public void UpdateReading(double airTemp) =>
public void UpdateReading(double airTemp) {
AirTemp = airTemp;
OnTemperatureChanged?.Invoke(airTemp);
}
}

[StateMachine]
public class Heater :
LogicBlock<Heater.Input, Heater.State, Heater.Output> {
public class Heater : LogicBlock<Heater.State> {
public Heater(ITemperatureSensor tempSensor) {
// Make sure states can access the temperature sensor.
Set(tempSensor);
}

public override State GetInitialState(IContext context) =>
new State.Off(context, 72.0);
new State.Off(context) { TargetTemp = 72.0 };

public abstract record Input {
public record TurnOn : Input;
public record TurnOff : Input;
public record TargetTempChanged(double Temp) : Input;
public record AirTempSensorChanged(double AirTemp) : Input;
public static class Input {
public readonly record struct TurnOn;
public readonly record struct TurnOff;
public readonly record struct TargetTempChanged(double Temp);
public readonly record struct AirTempSensorChanged(double AirTemp);
}

public abstract record State(IContext Context, double TargetTemp)
: StateLogic(Context) {
public record Off(
IContext Context, double TargetTemp
) : State(Context, TargetTemp), IGet<Input.TurnOn> {
State IGet<Input.TurnOn>.On(Input.TurnOn input) =>
new Heating(Context, TargetTemp);
}
public abstract record State : StateLogic, IGet<Input.TargetTempChanged> {
public double TargetTemp { get; init; }

public State(IContext context) : base(context) { }

public record Idle(IContext Context, double TargetTemp) :
State(Context, TargetTemp);
public State On(Input.TargetTempChanged input) => this with {
TargetTemp = input.Temp
};

public record Heating : State,
IGet<Input.TurnOff>,
IGet<Input.AirTempSensorChanged>,
IGet<Input.TargetTempChanged> {
public Heating(IContext context, double targetTemp) : base(
context, targetTemp
) {
public abstract record Powered : State, IGet<Input.TurnOff> {
public Powered(IContext context) : base(context) {
var tempSensor = context.Get<ITemperatureSensor>();

OnEnter<Heating>(
// When we enter the state, subscribe to changes in temperature.
OnEnter<Powered>(
(previous) => tempSensor.OnTemperatureChanged += OnTemperatureChanged
);

OnExit<Heating>(
// When we exit this state, unsubscribe from changes in temperature.
OnExit<Powered>(
(next) => tempSensor.OnTemperatureChanged -= OnTemperatureChanged
);
}

public State On(Input.TurnOff input) => new Off(Context, TargetTemp);
public State On(Input.TurnOff input) =>
new Off(Context) { TargetTemp = TargetTemp };

public State On(Input.AirTempSensorChanged input) =>
input.AirTemp >= TargetTemp
? new Idle(Context, TargetTemp)
: this;

public State On(Input.TargetTempChanged input) => this with {
TargetTemp = input.Temp
};

private void OnTemperatureChanged(double airTemp) {
// Whenever our temperature sensor gives us a reading, we will just
// provide an input to ourselves. This lets us have a chance to change
// the logic block's state.
private void OnTemperatureChanged(double airTemp) =>
Context.Input(new Input.AirTempSensorChanged(airTemp));
Context.Output(new Output.AirTempChanged(airTemp));
}
}
}

public abstract record Output {
public record AirTempChanged(double AirTemp) : Output;
}
}

public static class Program2 {
public static void Main2(string[] args) {
var tempSensor = new TemperatureSensor();
var heater = new Heater(tempSensor);

var binding = heater.Bind();
public record Off : State, IGet<Input.TurnOn> {
public Off(IContext context) : base(context) { }

binding.Handle<Heater.Output.AirTempChanged>(
(output) => Console.WriteLine($"Air temp changed to {output.AirTemp}")
);
public State On(Input.TurnOn input) {
// Get the temperature sensor from the blackboard.
var tempSensor = Context.Get<ITemperatureSensor>();

binding.When<Heater.State.Off>().Call(
(state) => Console.WriteLine("Heater is off")
);
if (tempSensor.AirTemp >= TargetTemp) {
// Room is already hot enough.
return new Idle(Context) { TargetTemp = TargetTemp };
}

binding.When<Heater.State.Idle>().Call(
(state) => Console.WriteLine("Heater is idle")
);
// Room is too cold — start heating.
return new Heating(Context) { TargetTemp = TargetTemp };
}
}

binding.When<Heater.State>()
.Use(
data: (state) => state.TargetTemp,
to: (temp) => Console.WriteLine($"Heater target temp is {temp}")
);
public record Idle : Powered, IGet<Input.AirTempSensorChanged> {
public Idle(IContext context) : base(context) { }

heater.Input(new Heater.Input.TurnOn());
public State On(Input.AirTempSensorChanged input) {
if (input.AirTemp < TargetTemp - 3.0d) {
// Temperature has fallen too far below target temp — start heating.
return new Heating(Context) { TargetTemp = TargetTemp };
}
// Room is still hot enough — keep waiting.
return this;
}
}

tempSensor.UpdateReading(58.0);
public record Heating : Powered, IGet<Input.AirTempSensorChanged> {
public Heating(IContext context) : base(context) { }

public State On(Input.AirTempSensorChanged input) {
if (input.AirTemp >= TargetTemp) {
// We're done heating!
Context.Output(new Output.FinishedHeating());
return new Idle(Context) { TargetTemp = TargetTemp };
}
// Room isn't hot enough — keep heating.
return this;
}
}
}

heater.Value.TargetTemp.ShouldBe(72);
public static class Output {
public readonly record struct FinishedHeating;
}
}
17 changes: 11 additions & 6 deletions Chickensoft.LogicBlocks.Generator.Tests/test_cases/Heater.g.puml
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
@startuml Heater
state "Heater State" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State {
state "Off" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off
state "Idle" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle
state "Heating" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating {
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating : OnTemperatureChanged() → AirTempChanged
state "Powered" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Powered {
state "Idle" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle
state "Heating" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating {
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating : OnAirTempSensorChangedFinishedHeating
}
}
state "Off" as Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off
}

Chickensoft_LogicBlocks_Generator_Tests_Heater_State --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State : TargetTempChanged
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating : AirTempSensorChanged
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating : TargetTempChanged
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle : AirTempSensorChanged
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off : TurnOff
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating : AirTempSensorChanged
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle : AirTempSensorChanged
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Heating : TurnOn
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Idle : TurnOn
Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Powered --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off : TurnOff

[*] --> Chickensoft_LogicBlocks_Generator_Tests_Heater_State_Off
@enduml
Loading

0 comments on commit be85467

Please sign in to comment.