393 lines
12 KiB
Plaintext
393 lines
12 KiB
Plaintext
@page "/"
|
|
@implements IDisposable
|
|
@using System.Threading
|
|
@using System.Net.Http.Json
|
|
@inject HttpClient Http
|
|
@inject LocalizationService Loc
|
|
|
|
<PageTitle>@Loc["app.title"]</PageTitle>
|
|
|
|
<div class="container">
|
|
<h1 class="mb-4">@Loc["app.title"]</h1>
|
|
|
|
<!-- READ-ONLY SECTION (Top) -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-light">
|
|
<strong>@Loc["section.visualization"]</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="status-panel mb-3">
|
|
<span class="badge bg-info text-dark me-2">@Loc["status.currentState"]: @Machine.CurrentState</span>
|
|
<span class="badge bg-secondary me-2">@Loc["status.steps"]: @Steps</span>
|
|
<span class="badge @(Machine.IsHalted ? "bg-danger" : "bg-success")">@Loc["status.status"]: @(Machine.IsHalted ? Loc["status.halted"] : Loc["status.ready"])</span>
|
|
</div>
|
|
|
|
<TapeVisualizer Tape="@Machine.Tape" HeadPosition="@Machine.HeadPosition" />
|
|
|
|
<Controls
|
|
OnStep="Step"
|
|
OnRunToggle="ToggleRun"
|
|
OnReset="Reset"
|
|
IsRunning="@IsRunning"
|
|
@bind-Speed="Speed" />
|
|
|
|
<TransitionTableVisualizer
|
|
Transitions="@Machine.Transitions"
|
|
CurrentState="@Machine.CurrentState"
|
|
CurrentReadSymbol="@Machine.Tape[Machine.HeadPosition]" />
|
|
|
|
<div class="mt-2">
|
|
<button class="btn btn-primary" @onclick="SetTape" disabled="@IsRunning">@Loc["button.loadConfig"]</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EDITABLE SECTION (Bottom) -->
|
|
<div class="card">
|
|
<div class="card-header bg-warning text-dark">
|
|
<strong>@Loc["section.config"]</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<!-- Examples -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">@Loc["label.examples"]</label>
|
|
<div @attributes="InertWhenRunning">
|
|
<select class="form-select @(IsRunning ? "input-disabled" : "")" value="@SelectedExampleIndex" @onchange="OnExampleChanged">
|
|
<option value="-1">@Loc["option.none"]</option>
|
|
@for (int i = 0; i < Examples.Count; i++)
|
|
{
|
|
<option value="@i">@(i + 1). @GetLocalizedText(Examples[i].Name)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<!-- Tape Configuration -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">@Loc["label.tapeState"]</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">@Loc["label.tapeValue"]</span>
|
|
<input type="text" class="form-control" @bind="TapeInput" placeholder="e.g. 1011" />
|
|
<span class="input-group-text">@Loc["label.headIndex"]</span>
|
|
<input type="number" class="form-control" @bind="HeadInput" style="max-width: 100px;" />
|
|
<button class="btn btn-primary" @onclick="SetTape" disabled="@IsRunning">@Loc["button.applyTape"]</button>
|
|
</div>
|
|
<small class="text-muted">@Loc["hint.tape"]</small>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<!-- Action Table Configuration -->
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">@Loc["label.actionTable"]</label>
|
|
<p class="text-muted small mb-1">@Loc["hint.format"] <code>@Loc["hint.csvFormat"]</code></p>
|
|
<textarea class="form-control mb-2 font-monospace" rows="6" @bind="CsvRulesInput" placeholder="q0, 1, q0, 0, L"></textarea>
|
|
|
|
@if (!string.IsNullOrEmpty(RuleParseError))
|
|
{
|
|
<div class="alert alert-danger py-2">@RuleParseError</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private Machine Machine { get; set; } = new();
|
|
private bool IsRunning { get; set; }
|
|
private int Speed { get; set; } = 500;
|
|
private int Steps { get; set; }
|
|
private CancellationTokenSource? _cts;
|
|
|
|
// UI Bindings
|
|
private string TapeInput { get; set; } = "";
|
|
private int HeadInput { get; set; } = 0;
|
|
|
|
// CSV Rules
|
|
private string CsvRulesInput { get; set; } = "";
|
|
private string RuleParseError { get; set; } = "";
|
|
|
|
// Add inert attribute to block all interaction while running
|
|
private Dictionary<string, object> InertWhenRunning =>
|
|
IsRunning ? new Dictionary<string, object> { ["inert"] = "" } : new();
|
|
|
|
// Example list loaded from server
|
|
private List<ExampleConfig> Examples { get; set; } = new();
|
|
private int SelectedExampleIndex { get; set; } = 0;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
Loc.OnLanguageChanged += OnLanguageChanged;
|
|
|
|
// Load example list from server
|
|
try
|
|
{
|
|
var examples = await Http.GetFromJsonAsync<List<ExampleConfig>>("api/examples");
|
|
if (examples != null && examples.Count > 0)
|
|
{
|
|
Examples = examples;
|
|
// Apply the first example as default
|
|
ApplyExample(Examples[0]);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Start with empty state if example loading fails
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when an example is selected from the ComboBox.
|
|
/// </summary>
|
|
private void OnExampleChanged(ChangeEventArgs e)
|
|
{
|
|
if (IsRunning) return;
|
|
if (int.TryParse(e.Value?.ToString(), out int index))
|
|
{
|
|
SelectedExampleIndex = index;
|
|
if (index >= 0 && index < Examples.Count)
|
|
{
|
|
ApplyExample(Examples[index]);
|
|
}
|
|
else
|
|
{
|
|
// Clear all values when "None" is selected
|
|
ClearConfiguration();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all control values in the Configuration section.
|
|
/// </summary>
|
|
private void ClearConfiguration()
|
|
{
|
|
_cts?.Cancel();
|
|
IsRunning = false;
|
|
|
|
TapeInput = "";
|
|
HeadInput = 0;
|
|
CsvRulesInput = "";
|
|
RuleParseError = "";
|
|
|
|
Machine = new Machine();
|
|
Steps = 0;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies an example configuration to the UI.
|
|
/// </summary>
|
|
private void ApplyExample(ExampleConfig example)
|
|
{
|
|
_cts?.Cancel();
|
|
IsRunning = false;
|
|
|
|
TapeInput = example.TapeInput;
|
|
HeadInput = example.HeadPosition;
|
|
CsvRulesInput = BuildCsvFromActions(example.Actions);
|
|
|
|
LoadRules();
|
|
SetTape();
|
|
|
|
Steps = 0;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the value matching the current language from a localized text list.
|
|
/// </summary>
|
|
private string GetLocalizedText(List<LocalizedText> texts)
|
|
{
|
|
return texts.FirstOrDefault(t => t.Language == Loc.CurrentLanguage)?.Value
|
|
?? texts.FirstOrDefault()?.Value
|
|
?? "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the Actions array to a CSV string with comments in the current language.
|
|
/// </summary>
|
|
private string BuildCsvFromActions(List<ActionEntry> actions)
|
|
{
|
|
var lines = new List<string>();
|
|
foreach (var action in actions)
|
|
{
|
|
var comment = GetLocalizedText(action.Comment);
|
|
lines.Add(string.IsNullOrEmpty(comment)
|
|
? action.Value
|
|
: $"{action.Value}, {comment}");
|
|
}
|
|
return string.Join("\n", lines);
|
|
}
|
|
|
|
private void LoadRules()
|
|
{
|
|
RuleParseError = "";
|
|
var newTransitions = new List<(string, char, string, char, MoveDirection, string)>();
|
|
var seenKeys = new Dictionary<(string, char), int>();
|
|
var lines = CsvRulesInput.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
|
int lineNum = 0;
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
lineNum++;
|
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
|
|
var parts = line.Split(',', 6);
|
|
if (parts.Length < 5 || parts.Length > 6)
|
|
{
|
|
RuleParseError = Loc["error.columnCount", lineNum, parts.Length];
|
|
return;
|
|
}
|
|
|
|
var state = parts[0].Trim();
|
|
var readStr = parts[1].Trim();
|
|
var newState = parts[2].Trim();
|
|
var writeStr = parts[3].Trim();
|
|
var moveStr = parts[4].Trim();
|
|
|
|
if (readStr.Length != 1)
|
|
{
|
|
RuleParseError = Loc["error.readSymbol", lineNum, readStr];
|
|
return;
|
|
}
|
|
if (writeStr.Length != 1)
|
|
{
|
|
RuleParseError = Loc["error.writeSymbol", lineNum, writeStr];
|
|
return;
|
|
}
|
|
|
|
MoveDirection move;
|
|
if (moveStr.Equals("L", StringComparison.OrdinalIgnoreCase) || moveStr.Equals("Left", StringComparison.OrdinalIgnoreCase))
|
|
move = MoveDirection.Left;
|
|
else if (moveStr.Equals("R", StringComparison.OrdinalIgnoreCase) || moveStr.Equals("Right", StringComparison.OrdinalIgnoreCase))
|
|
move = MoveDirection.Right;
|
|
else if (moveStr.Equals("S", StringComparison.OrdinalIgnoreCase) || moveStr.Equals("Stay", StringComparison.OrdinalIgnoreCase))
|
|
move = MoveDirection.Stay;
|
|
else
|
|
{
|
|
RuleParseError = Loc["error.moveDirection", lineNum, moveStr];
|
|
return;
|
|
}
|
|
|
|
// 6th column is an optional comment
|
|
string comment = parts.Length > 5 ? parts[5].Trim() : "";
|
|
|
|
var key = (state, readStr[0]);
|
|
if (seenKeys.TryGetValue(key, out int prevLine))
|
|
{
|
|
RuleParseError = Loc["error.duplicateKey", lineNum, state, readStr, prevLine];
|
|
return;
|
|
}
|
|
seenKeys[key] = lineNum;
|
|
|
|
newTransitions.Add((state, readStr[0], newState, writeStr[0], move, comment));
|
|
}
|
|
|
|
if (newTransitions.Count == 0)
|
|
{
|
|
RuleParseError = Loc["error.noRules"];
|
|
return;
|
|
}
|
|
|
|
// Assume initial state is the first state mentioned in the rules
|
|
string initialState = newTransitions.First().Item1;
|
|
|
|
Machine.LoadProgram(initialState, newTransitions);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void SetTape()
|
|
{
|
|
_cts?.Cancel();
|
|
IsRunning = false;
|
|
|
|
// Also apply CSV rules
|
|
LoadRules();
|
|
|
|
Machine.Reset();
|
|
|
|
// Apply initial tape values
|
|
if (!string.IsNullOrEmpty(TapeInput))
|
|
{
|
|
for (int i = 0; i < TapeInput.Length; i++)
|
|
{
|
|
Machine.Tape[i] = TapeInput[i];
|
|
}
|
|
}
|
|
|
|
Machine.ForceHeadPosition(HeadInput);
|
|
Steps = 0;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void Step()
|
|
{
|
|
if (Machine.IsHalted) return;
|
|
|
|
Machine.Step();
|
|
Steps++;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task ToggleRun(bool shouldRun)
|
|
{
|
|
IsRunning = shouldRun;
|
|
if (IsRunning)
|
|
{
|
|
_cts = new CancellationTokenSource();
|
|
try
|
|
{
|
|
await RunLoop(_cts.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Stopped
|
|
}
|
|
finally
|
|
{
|
|
IsRunning = false;
|
|
_cts = null;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_cts?.Cancel();
|
|
}
|
|
}
|
|
|
|
private async Task RunLoop(CancellationToken token)
|
|
{
|
|
while (!token.IsCancellationRequested && !Machine.IsHalted)
|
|
{
|
|
Step();
|
|
await Task.Delay(Speed, token);
|
|
}
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
// Re-apply the current inputs to simulate a "Reset to Initial"
|
|
SetTape();
|
|
}
|
|
|
|
private void OnLanguageChanged()
|
|
{
|
|
// Rebuild comments in the new language if an example is selected
|
|
if (SelectedExampleIndex >= 0 && SelectedExampleIndex < Examples.Count)
|
|
{
|
|
CsvRulesInput = BuildCsvFromActions(Examples[SelectedExampleIndex].Actions);
|
|
}
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Loc.OnLanguageChanged -= OnLanguageChanged;
|
|
}
|
|
}
|