diff --git a/experiments/C/src/TugOfWar/Interface/app.py b/experiments/C/src/TugOfWar/Interface/app.py new file mode 100755 index 00000000..88adec5e --- /dev/null +++ b/experiments/C/src/TugOfWar/Interface/app.py @@ -0,0 +1,239 @@ +import pandas as pd +import dash +from dash import dcc, html, Input, Output +from dash.exceptions import PreventUpdate +import plotly.express as px +from flask import Flask, request +import json +import app_helper as ap +import dash_bootstrap_components as dbc +import dash_daq as daq + + +player_force = dict({ + 'players': ['Team_A_Player_0', 'Team_A_Player_1', 'Team_B_Player_0', 'Team_B_Player_1'], + 'forces': [0, 0, 0, 0], + 'score': 'Advantage: None', + 'rope_mark': 20 +}) + +print(player_force) + +limit1 = 5 +limit2 = 35 +rope_range = 40 + + +# Create Flask server +server = Flask(__name__) + +# Create Dash app +app = dash.Dash(__name__, server=server,external_stylesheets=[dbc.themes.BOOTSTRAP]) + +df = ap.build_rope_dataframe(limit1, limit2, player_force['rope_mark']) + +# Define layout +app.layout = html.Div([ + dbc.Card( + dbc.CardBody([ + + # Row 1: title + dbc.Row([ + dbc.Col([ + html.Div( + dbc.CardImg(src='assets/lf.png', + style={'backgroundColor': '#013163', 'width': '40%', 'height': '40%'}, + id='logo' + ) + , style={'align': 'center'}) + ], align='center', width=2), + dbc.Col([ + html.Div(ap.drawTextCard("TUG OF WAR"), + id='title'), + ], width=10), + ], style={'backgroundColor': '#013163'}, align='center'), + html.Br(), + + ### Row 2: Score + dbc.Row([ + dbc.Col([ + html.Div( + dbc.Card([ + dbc.CardBody([ + html.Div([ + dcc.Input(id='score', value=player_force['score'], type="text", style={'fontSize': '20px'}) + ], style={'textAlign': 'center'}) + ]) + ]) + ), + ], width=12), + ], align='center'), + html.Br(), + + ### Row 3: Rope + dbc.Row([ + dbc.Col([ + html.Div( + dbc.Card( + dbc.CardBody([ + dcc.Graph( + id='rope', + figure=px.scatter(df, + x="x", + y="y", + color="val", + symbol='val', + size='val', + range_x=[1,rope_range], + range_y=[0, 0] + ).update_layout( + template='plotly_dark', + plot_bgcolor= 'rgba(0, 0, 0, 0)', + paper_bgcolor= 'rgba(0, 0, 0, 0)', + xaxis={'visible':False}, + coloraxis={'showscale':False} + ), + config={ + 'displayModeBar': False + } + + ) + ]) + ), + ) + ], width=12), + ], align='center'), + html.Br(), + + ### Row 4: Gauges + dbc.Row([ + ## Gauge 1 + dbc.Col([ + html.Div( + dbc.Card([ + dbc.CardBody([ + html.Div([ + daq.Gauge( + value=player_force['forces'][0], + label=player_force['players'][0], + max=20, + min=0, id='gauge1' + ) + ], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) + ]) + ]) + ), + ], width=3), + + ## Gauge 2 + dbc.Col([ + html.Div( + dbc.Card([ + dbc.CardBody([ + html.Div([ + daq.Gauge( + value=player_force['forces'][1], + label=player_force['players'][1], + max=20, + min=0, id='gauge2' + ) + ], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) + ]) + ]) + ), + ], width=3), + + ## Gauge 3 + dbc.Col([ + html.Div( + dbc.Card([ + dbc.CardBody([ + html.Div([ + daq.Gauge( + value=player_force['forces'][2], + label=player_force['players'][2], + max=20, + min=0, id='gauge3' + ) + ], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) + ]) + ]) + ), + ], width=3), + + ## Gauge 4 + dbc.Col([ + html.Div( + dbc.Card([ + dbc.CardBody([ + html.Div([ + daq.Gauge( + value=player_force['forces'][3], + label=player_force['players'][3], + max=20, + min=0, id='gauge4' + ) + ], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) + ]) + ]) + ), + ], width=3), + ],align='center'), + + ]) + ), dcc.Interval(id='interval-component', interval=1000, n_intervals=0) +]) + +@app.callback([ + Output('gauge1', 'value'), + Output('gauge2', 'value'), + Output('gauge3', 'value'), + Output('gauge4', 'value'), + Output('rope', 'figure'), + Output('score', 'value') + ], + Input('interval-component', 'n_intervals') +) +def update_layout(n): + global player_force + global rope_range + global limit1 + global limit2 + df = ap.build_rope_dataframe(limit1, limit2, player_force['rope_mark']) + fig = px.scatter(df, + x="x", + y="y", + color="val", + symbol='val', + size='val', + range_x=[1,rope_range], + range_y=[0, 0] + ).update_layout( + template='plotly_dark', + plot_bgcolor= 'rgba(0, 0, 0, 0)', + paper_bgcolor= 'rgba(0, 0, 0, 0)', + xaxis={'visible':False}, + coloraxis={'showscale':False} + ) + return player_force['forces'][0], \ + player_force['forces'][1], \ + player_force['forces'][2], \ + player_force['forces'][3], \ + fig, \ + player_force['score'] + +@server.route('/update_force', methods=['POST']) +def update_force(): + global player_force + try: + data = json.loads(request.data) + player_force = data[0] + print(player_force) + # player_force['players'] = data['players'] + # player_force['rope_mark'] = int(data['rope_mark']) + return 'Force values updated successfully' + except Exception as e: + return str(e), 400 + +if __name__ == '__main__': + server.run(port=5004) diff --git a/experiments/C/src/TugOfWar/Interface/app_helper.py b/experiments/C/src/TugOfWar/Interface/app_helper.py new file mode 100755 index 00000000..7f14b53b --- /dev/null +++ b/experiments/C/src/TugOfWar/Interface/app_helper.py @@ -0,0 +1,63 @@ +from dash import dcc, html +import dash_bootstrap_components as dbc +import plotly.express as px +import dash_daq as daq +import pandas as pd +import plotly.graph_objects as go + +def drawFigureCard(l1, l2, m): + df = build_rope_dataframe(l1, l2, m) + return dbc.Card( + dbc.CardBody([ + dcc.Graph( + figure=px.scatter(df, + x="x", + y="y", + color="val", + symbol='val', + size='val', + range_x=[0,40], + range_y=[0, 0] + ).update_layout( + template='plotly_dark', + plot_bgcolor= 'rgba(0, 0, 0, 0)', + paper_bgcolor= 'rgba(0, 0, 0, 0)', + xaxis={'visible':False}, + coloraxis={'showscale':False} + ), + config={ + 'displayModeBar': False + } + ) + ]) + ) + +# Title field +def drawTextCard(tt): + return dbc.Card( + dbc.CardBody([ + html.Div([ + html.H1(tt, style={'color': 'white'}), + ], style={'textAlign': 'center'}) + ], style={'backgroundColor': '#013163'}) + ) + +# Build the dataframe for the heatmap +def build_rope_dataframe(limit1, limit2, mark): + # Create a DataFrame of zeros + # Create lists for 'x', 'y', and 'val' + x_values = list(range(1, 39)) + y_values = [0] + val_values = [0] * len(x_values) + data = {'x': [], 'y': [], 'val': []} + for y in y_values: + data['x'].extend(x_values) + data['y'].extend([y] * len(x_values)) + data['val'].extend(val_values) + df = pd.DataFrame(data) + # Render the limits + df.loc[(df['x'] == limit1) & (df['y'] == 0), 'val'] = 50 + df.loc[(df['x'] == limit2) & (df['y'] == 0), 'val'] = 50 + # # Render the mark + df.loc[(df['x'] == mark) & (df['y'] == 0), 'val'] = 100 + return df diff --git a/experiments/C/src/TugOfWar/Interface/assets/lf.png b/experiments/C/src/TugOfWar/Interface/assets/lf.png new file mode 100755 index 00000000..5c6bd98d Binary files /dev/null and b/experiments/C/src/TugOfWar/Interface/assets/lf.png differ diff --git a/experiments/C/src/TugOfWar/Interface/request.sh b/experiments/C/src/TugOfWar/Interface/request.sh new file mode 100755 index 00000000..d48afe7d --- /dev/null +++ b/experiments/C/src/TugOfWar/Interface/request.sh @@ -0,0 +1,7 @@ +curl -X POST http://127.0.0.1:5004/update_force -H "Content-Type: application/json" -d '[ + {"players": ["Team_A_Player_0", "Team_A_Player_1", "Team_B_Player_0", "Team_B_Player_1"], + "forces": [12, 13, 11, 12], + "score": "ttt", + "rope_mark": 1 + } +]' diff --git a/experiments/C/src/TugOfWar/Interface/requirements.txt b/experiments/C/src/TugOfWar/Interface/requirements.txt new file mode 100755 index 00000000..1d357f30 --- /dev/null +++ b/experiments/C/src/TugOfWar/Interface/requirements.txt @@ -0,0 +1,8 @@ +flask==2.2.0 +dash==2.11.1 +Werkzeug==2.2.0 +pandas +plotly_express +dash_bootstrap_components +dash_daq +setuptools diff --git a/experiments/C/src/TugOfWar/Interface/start.sh b/experiments/C/src/TugOfWar/Interface/start.sh new file mode 100755 index 00000000..16760419 --- /dev/null +++ b/experiments/C/src/TugOfWar/Interface/start.sh @@ -0,0 +1,18 @@ +#!/bin/bash +if [ ! -f ".lf_env/bin/activate" ] +then + # Create a virtual environment + virtualenv .venv + + # Activate the virtual environment + source .venv/bin/activate + + # Install Flask + pip install -r requirements.txt +else + # Activate the virtual environment + source .venv/bin/activate +fi + +# Start the Flask and Dash application +python3 app.py \ No newline at end of file diff --git a/experiments/C/src/TugOfWar/Player.lf b/experiments/C/src/TugOfWar/Player.lf new file mode 100755 index 00000000..10b3ad1e --- /dev/null +++ b/experiments/C/src/TugOfWar/Player.lf @@ -0,0 +1,17 @@ +target C + +preamble {= + #include +=} + +reactor Player(max_force: int = 7) { + output out: int + timer t(0, 1 s) + + reaction(t) -> out {= + int lower = 1; + int force = (rand() % (self->max_force - lower + 1)) + lower; + lf_print("Force = %d", force); + lf_set(out, force); + =} +} diff --git a/experiments/C/src/TugOfWar/README.md b/experiments/C/src/TugOfWar/README.md new file mode 100644 index 00000000..c99eaae4 --- /dev/null +++ b/experiments/C/src/TugOfWar/README.md @@ -0,0 +1,17 @@ +## Tug of War + +Tug of War is a team-based game where two teams pull on opposite ends of a rope to bring the other team across a center marker. The demo involves two players per team, each characterized by a parameter. Players apply a randomly generated force within the interval 1 and a specified maximum force parameter. The sum of forces on each side moves the marker, and when it reaches one of the limits, the game ends. + +## How it works + +This example features a browser-based UI. The server is constructed using [Flask](https://flask.palletsprojects.com/en/3.0.x/), a Python web framework, and [Dash](https://dash.plotly.com/) components. + +Every second, player forces are generated and their values are send to the server as +a post request. The UI's gauges are updated with these values, along with the position of the mark (yellow square). When the mark reaches one of the limits (pink diamond), the label on the top of the page is updated with the result. + +## Steps: + + 1. Compile `TugOfWar.lf`. + 2. Launch the UI by running the script `start.sh` in the `C/src/TugOfWar/Interface` directory. The script will create a virtual environment and install the requirements listed in `requirements.txt`, if not there already. It then lauches Flask server, accessible on: [http://127.0.0.1:5004](http://127.0.0.1:5004). + 3. Run the launching script under `bin` and watch the game on the UI. + diff --git a/experiments/C/src/TugOfWar/TugOfWar.lf b/experiments/C/src/TugOfWar/TugOfWar.lf new file mode 100755 index 00000000..1a774860 --- /dev/null +++ b/experiments/C/src/TugOfWar/TugOfWar.lf @@ -0,0 +1,21 @@ +target C { + keepalive: true +} + +import Player from "Player.lf" +import TugOfWarGame from "TugOfWarGame.lf" + +preamble {= + #include + #include +=} + +federated reactor TugOfWar { + towg = new TugOfWarGame() + p1 = new Player(max_force=5) + p2 = new Player(max_force=6) + p3 = new Player(max_force=3) + p4 = new Player(max_force=9) + + p1.out, p2.out, p3.out, p4.out -> towg.force +} diff --git a/experiments/C/src/TugOfWar/TugOfWarGame.lf b/experiments/C/src/TugOfWar/TugOfWarGame.lf new file mode 100755 index 00000000..3871000f --- /dev/null +++ b/experiments/C/src/TugOfWar/TugOfWarGame.lf @@ -0,0 +1,111 @@ +target C + +preamble {= + #include + #include +=} + +reactor TugOfWarGame { + input[4] force: int // Each player will exert a force + + timer t(0, 1 s) // Timer to compute the total status and send the display request + + state agent_force: int[4] = {0, 0, 0, 0} // States + state rope_mark: int = 20 + state updated: bool = false + + reaction(startup) {= + // Construct the data to send + char curl_cmd[1024]; + sprintf(curl_cmd, + "curl -X POST http://127.0.0.1:5004/update_force -H \"Content-Type: application/json\" -d '[ \ + {\"players\": [\"Team_A_Player_0\", \"Team_A_Player_1\", \"Team_B_Player_0\", \"Team_B_Player_1\"], \ + \"forces\": [0, 0, 0, 0], \ + \"score\": \"Advantage: None\", \ + \"rope_mark\": 20 \ + }\ + ]' \ + " + ); + int status = system(curl_cmd); + if (status == 0) { + lf_print("Updates successfully sent."); + } else { + lf_print("Unable to send update."); + } + =} + + reaction(force) {= + int sum = 0; + for (int i = 0; i < 4; i++) { + if (force[i]->is_present) { + self->agent_force[i] = force[i]->value; + self->updated = true; + } + } + =} + + reaction(t) {= + if (self->updated) { + // Compute the new rope mark position + self->rope_mark = self->rope_mark - self->agent_force[0] + - self->agent_force[1] + + self->agent_force[2] + + self->agent_force[3]; + + // Derive the new score + char score[25]; + if (self->rope_mark <= 5) { + sprintf(score, "Winner: Team A"); + } else if (self->rope_mark >= 35) { + sprintf(score, "Winner: Team B"); + } else if (self->rope_mark == 20) { + sprintf(score, "Advantage: None"); + } else if (self->rope_mark < 20) { + sprintf(score, "Advantage: Team A"); + } else { + sprintf(score, "Advantage: Team B"); + } + + + // Construct the data to send + char curl_cmd[1024]; + + sprintf(curl_cmd, + "curl -X POST http://127.0.0.1:5004/update_force -H \"Content-Type: application/json\" -d '[ \ + {\"players\": [\"Team_A_Player_0\", \"Team_A_Player_1\", \"Team_B_Player_0\", \"Team_B_Player_1\"], \ + \"forces\": [%d, %d, %d, %d], \ + \"score\": \"%s\", \ + \"rope_mark\": %d \ + }\ + ]' \ + ", + self->agent_force[0], + self->agent_force[1], + self->agent_force[2], + self->agent_force[3], + score, + self->rope_mark + ); + + int status = system(curl_cmd); + if (status == 0) { + lf_print("Updates successfully sent."); + } else { + lf_print("Unable to send update."); + } + + self->updated = false; + + // Reset all forces + for (int i = 0; i < 4; i++) { + self->agent_force[i] = 0; + } + + // If one of team won, stop the game + if (self->rope_mark<= 5 || self->rope_mark >= 35) { + lf_request_stop(); + } + } + =} +}