added Exam files
This commit is contained in:
parent
9cc814bdb4
commit
637aa2890b
15
Exam/Dockerfile
Normal file
15
Exam/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
# Dockerfile
|
||||
FROM nginx:alpine
|
||||
|
||||
# Remove default content
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
|
||||
# Copy your simulator files
|
||||
COPY . /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Run Nginx in foreground
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
15
Exam/Exam/Dockerfile
Normal file
15
Exam/Exam/Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
# Dockerfile
|
||||
FROM nginx:alpine
|
||||
|
||||
# Remove default content
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
|
||||
# Copy your simulator files
|
||||
COPY . /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Run Nginx in foreground
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
6
Exam/disable-maintenance.sh
Executable file
6
Exam/disable-maintenance.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
# deploy the real site back
|
||||
netlify deploy --prod --dir=. \
|
||||
--message="✅ Live site ON"
|
||||
|
14
Exam/docker-compose.yml
Normal file
14
Exam/docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
services:
|
||||
simulator:
|
||||
build: .
|
||||
image: karel-simulator
|
||||
container_name: karel-simulator
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: karel-redis
|
||||
restart: unless-stopped
|
||||
|
6
Exam/enable-maintenance.sh
Executable file
6
Exam/enable-maintenance.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
# deploy the maintenance folder
|
||||
netlify deploy --prod --dir=maintenance \
|
||||
--message="⚙️ Maintenance page ON"
|
||||
|
849
Exam/index.html
Executable file
849
Exam/index.html
Executable file
@ -0,0 +1,849 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Karel Simulator</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #121212;
|
||||
--surface: #1e1e1e;
|
||||
--border: #2a2a2a;
|
||||
--text: #e0e0e0;
|
||||
--accent: #0f0;
|
||||
--wall: #d32f2f;
|
||||
--hover: rgba(255,255,255,0.05);
|
||||
--radius: 4px;
|
||||
}
|
||||
*,*::before,*::after{box-sizing:border-box;}
|
||||
body {
|
||||
margin:0;padding:20px;
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
font-family:monospace;
|
||||
line-height:1.4;
|
||||
}
|
||||
h1,h2,p,label{color:var(--text);}
|
||||
#container,#mapEditorContainer {
|
||||
display:flex;gap:20px;
|
||||
}
|
||||
#leftPane,#mapEditorControls{flex:1;max-width:600px;}
|
||||
#rightPane,#editorMap{flex:1;}
|
||||
#editor,#console,#editorMap,#map {
|
||||
width:100%;
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius);
|
||||
padding:12px;
|
||||
margin-bottom:20px;
|
||||
white-space:pre;
|
||||
color:var(--text);
|
||||
font-family:monospace;
|
||||
overflow:auto;
|
||||
}
|
||||
#editor{height:200px;}
|
||||
#console{height:150px;color:var(--accent);white-space:pre-wrap;}
|
||||
button,input[type="number"],select {
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius);
|
||||
color:var(--text);
|
||||
padding:8px 16px;
|
||||
font:inherit;
|
||||
cursor:pointer;
|
||||
transition:background 0.2s;
|
||||
margin-right:10px;
|
||||
}
|
||||
button:hover,input[type="number"]:hover,select:hover {
|
||||
background:var(--hover);
|
||||
}
|
||||
button:active{background:var(--border);}
|
||||
input[type="file"]{
|
||||
background:none;border:none;color:var(--text);
|
||||
}
|
||||
#map{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
color:var(--text);
|
||||
}
|
||||
#status{display:flex;gap:20px;margin-top:10px;}
|
||||
.status-card{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius);
|
||||
padding:8px 12px;
|
||||
display:flex;align-items:center;
|
||||
}
|
||||
.status-card .label{color:var(--accent);margin-right:6px;}
|
||||
.map-border{color:var(--accent);}
|
||||
.cell-content{color:var(--accent);}
|
||||
.wall-border{color:var(--wall);}
|
||||
/* Editor spacing */
|
||||
#mapEditorControls{display:flex;flex-direction:column;gap:16px;}
|
||||
#mapEditorControls h2{margin-bottom:0;}
|
||||
#mapEditorControls > label,
|
||||
#mapEditorControls > input[type="number"],
|
||||
#mapEditorControls > select,
|
||||
#mapEditorControls > button {
|
||||
margin:4px 0;
|
||||
}
|
||||
.edit-mode-group {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:8px;
|
||||
}
|
||||
.clickable-wall{
|
||||
display:inline-block;
|
||||
transition:background 0.2s;
|
||||
cursor:pointer;
|
||||
}
|
||||
.clickable-wall:hover{
|
||||
background:var(--hover);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Karel Simulator</h1>
|
||||
|
||||
<!-- Main Simulator View -->
|
||||
<div id="container">
|
||||
<div id="leftPane">
|
||||
<p>
|
||||
Write your Karel program below. Parentheses & semicolons mandatory.
|
||||
If you’re ever unsure how to use the Simulator,
|
||||
<a href="manual.html" target="_blank" style="color:var(--accent);text-decoration:underline;">
|
||||
consult the manual
|
||||
</a>.
|
||||
</p>
|
||||
<textarea id="editor">// Example:
|
||||
if (frontIsBlocked()) {
|
||||
turnLeft();
|
||||
} else {
|
||||
move();
|
||||
}
|
||||
</textarea>
|
||||
<button id="runButton">Compile & Run</button>
|
||||
<button id="stopButton">Stop</button>
|
||||
<button id="editMapButton">Edit Map</button>
|
||||
<h2>Console Output:</h2>
|
||||
<div id="console"></div>
|
||||
<h2>Upload Map File:</h2>
|
||||
<input type="file" id="mapFileInput" accept=".txt,.kw"/>
|
||||
<button id="uploadMapButton">Upload Map</button>
|
||||
</div>
|
||||
|
||||
<div id="rightPane">
|
||||
<h2>Karel Map:</h2>
|
||||
<div id="map"></div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Map Editor -->
|
||||
<div id="mapEditorContainer" style="display:none;">
|
||||
<div id="mapEditorControls">
|
||||
<h2>Manual Map Editor</h2>
|
||||
|
||||
<label>
|
||||
Width:
|
||||
<input type="number" id="editWidth" min="3" value="10"/>
|
||||
</label>
|
||||
<label>
|
||||
Height:
|
||||
<input type="number" id="editHeight" min="3" value="10"/>
|
||||
</label>
|
||||
<button id="generateMapButton">Generate Map</button>
|
||||
|
||||
<div class="edit-mode-group">
|
||||
<label><input type="radio" name="editMode" value="wall" checked/> Toggle Wall</label>
|
||||
<label><input type="radio" name="editMode" value="beeper"/> Beeper (click to add, Ctrl+click to remove)</label>
|
||||
<label><input type="radio" name="editMode" value="karel"/> Set Karel</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Karel Dir:
|
||||
<select id="karelDirSelect">
|
||||
<option value="north">North</option>
|
||||
<option value="east">East</option>
|
||||
<option value="south">South</option>
|
||||
<option value="west">West</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<!-- NEW: Beeper Bag Count -->
|
||||
<label>
|
||||
Beeper Bag:
|
||||
<input type="number" id="beeperBagInput" min="0" max="99" value="0"/>
|
||||
</label>
|
||||
|
||||
<button id="importEditorMapButton">Import to Simulator</button>
|
||||
<button id="discardEditorMapButton">Discard Changes</button>
|
||||
</div>
|
||||
<div id="editorMap"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ───────── Globals & State ─────────
|
||||
let uploadedMapData = null,
|
||||
currentWorld, currentKarel,
|
||||
editorMapData = null,
|
||||
STEP_DELAY = 300,
|
||||
stopRequested = false,
|
||||
editMode = 'wall';
|
||||
|
||||
function log(msg){
|
||||
document.getElementById('console').textContent += msg + '\n';
|
||||
}
|
||||
function sleep(ms){
|
||||
return new Promise(r=>setTimeout(r,ms));
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 1. LEXER
|
||||
**********************************************************************/
|
||||
class Token { constructor(type,value){ this.type=type; this.value=value; } }
|
||||
const TokenType={ IDENTIFIER:'IDENTIFIER',KEYWORD:'KEYWORD',SYMBOL:'SYMBOL',EOF:'EOF' };
|
||||
const keywords=new Set(['if','else','while']);
|
||||
class Lexer {
|
||||
constructor(input){ this.input=input; this.pos=0; }
|
||||
skipWhitespace(){
|
||||
while(this.pos<this.input.length && /\s/.test(this.input[this.pos]))
|
||||
this.pos++;
|
||||
}
|
||||
nextToken(){
|
||||
this.skipWhitespace();
|
||||
if(this.pos>=this.input.length) return new Token(TokenType.EOF,'');
|
||||
const ch=this.input[this.pos];
|
||||
if(/[A-Za-z_]/.test(ch)){
|
||||
let start=this.pos;
|
||||
while(this.pos<this.input.length && /[A-Za-z0-9_]/.test(this.input[this.pos]))
|
||||
this.pos++;
|
||||
const w=this.input.slice(start,this.pos);
|
||||
return new Token(keywords.has(w)?TokenType.KEYWORD:TokenType.IDENTIFIER,w);
|
||||
}
|
||||
if('(){};'.includes(ch)){
|
||||
this.pos++;
|
||||
return new Token(TokenType.SYMBOL,ch);
|
||||
}
|
||||
this.pos++;
|
||||
return this.nextToken();
|
||||
}
|
||||
tokenize(){
|
||||
const tokens=[];
|
||||
let tok;
|
||||
do{ tok=this.nextToken(); tokens.push(tok); }
|
||||
while(tok.type!==TokenType.EOF);
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 2. PARSER
|
||||
**********************************************************************/
|
||||
class ProgramNode{ constructor(stmts){ this.statements=stmts; } }
|
||||
class ExpressionStatement{ constructor(expr){ this.expr=expr; } }
|
||||
class IfStatement{ constructor(cond,thenB,elseB){
|
||||
this.condition=cond; this.thenBlock=thenB; this.elseBlock=elseB;
|
||||
}}
|
||||
class WhileStatement{ constructor(cond,blk){ this.condition=cond; this.block=blk; } }
|
||||
class BlockStatement{ constructor(stmts){ this.statements=stmts; } }
|
||||
class FunctionCall{ constructor(name){ this.name=name; } }
|
||||
|
||||
class Parser {
|
||||
constructor(tokens){ this.tokens=tokens; this.pos=0; }
|
||||
cur(){ return this.tokens[this.pos]; }
|
||||
eat(type,val=null){
|
||||
const t=this.cur();
|
||||
if(t.type!==type||(val&&t.value!==val))
|
||||
throw new Error(`Unexpected token: ${t.value}`);
|
||||
this.pos++;
|
||||
return t;
|
||||
}
|
||||
parseProgram(){
|
||||
const stmts=[];
|
||||
while(this.cur().type!==TokenType.EOF){
|
||||
stmts.push(this.parseStatement());
|
||||
}
|
||||
return new ProgramNode(stmts);
|
||||
}
|
||||
parseStatement(){
|
||||
const t=this.cur();
|
||||
if(t.type===TokenType.KEYWORD&&t.value==='if') return this.parseIf();
|
||||
if(t.type===TokenType.KEYWORD&&t.value==='while') return this.parseWhile();
|
||||
if(t.type===TokenType.SYMBOL&&t.value==='{') return this.parseBlock();
|
||||
const expr=this.parseExpression();
|
||||
this.eat(TokenType.SYMBOL,';');
|
||||
return new ExpressionStatement(expr);
|
||||
}
|
||||
parseIf(){
|
||||
this.eat(TokenType.KEYWORD,'if');
|
||||
this.eat(TokenType.SYMBOL,'(');
|
||||
const cond=this.parseExpression();
|
||||
this.eat(TokenType.SYMBOL,')');
|
||||
const thenB=this.parseStatement();
|
||||
let elseB=null;
|
||||
if(this.cur().type===TokenType.KEYWORD&&this.cur().value==='else'){
|
||||
this.eat(TokenType.KEYWORD,'else');
|
||||
elseB=this.parseStatement();
|
||||
}
|
||||
return new IfStatement(cond,thenB,elseB);
|
||||
}
|
||||
parseWhile(){
|
||||
this.eat(TokenType.KEYWORD,'while');
|
||||
this.eat(TokenType.SYMBOL,'(');
|
||||
const cond=this.parseExpression();
|
||||
this.eat(TokenType.SYMBOL,')');
|
||||
const blk=this.parseStatement();
|
||||
return new WhileStatement(cond,blk);
|
||||
}
|
||||
parseBlock(){
|
||||
this.eat(TokenType.SYMBOL,'{');
|
||||
const stmts=[];
|
||||
while(!(this.cur().type===TokenType.SYMBOL&&this.cur().value==='}')){
|
||||
stmts.push(this.parseStatement());
|
||||
}
|
||||
this.eat(TokenType.SYMBOL,'}');
|
||||
return new BlockStatement(stmts);
|
||||
}
|
||||
parseExpression(){ return this.parseFunctionCall(); }
|
||||
parseFunctionCall(){
|
||||
const name=this.eat(TokenType.IDENTIFIER).value;
|
||||
this.eat(TokenType.SYMBOL,'(');
|
||||
this.eat(TokenType.SYMBOL,')');
|
||||
return new FunctionCall(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 3. INTERPRETER (async + stop)
|
||||
**********************************************************************/
|
||||
async function executeProgramAsync(prog,ctx){
|
||||
for(const stmt of prog.statements){
|
||||
if(stopRequested) throw new Error("Execution stopped by user");
|
||||
await executeStatementAsync(stmt,ctx);
|
||||
}
|
||||
}
|
||||
async function executeStatementAsync(stmt,ctx){
|
||||
if(stopRequested) throw new Error("Execution stopped by user");
|
||||
if(stmt instanceof ExpressionStatement){
|
||||
await evaluateExpressionAsync(stmt.expr,ctx);
|
||||
} else if(stmt instanceof IfStatement){
|
||||
if(await evaluateExpressionAsync(stmt.condition,ctx)){
|
||||
await executeStatementAsync(stmt.thenBlock,ctx);
|
||||
} else if(stmt.elseBlock){
|
||||
await executeStatementAsync(stmt.elseBlock,ctx);
|
||||
}
|
||||
} else if(stmt instanceof WhileStatement){
|
||||
while(await evaluateExpressionAsync(stmt.condition,ctx)){
|
||||
if(stopRequested) throw new Error("Execution stopped by user");
|
||||
await executeStatementAsync(stmt.block,ctx);
|
||||
}
|
||||
} else if(stmt instanceof BlockStatement){
|
||||
for(const s of stmt.statements){
|
||||
if(stopRequested) throw new Error("Execution stopped by user");
|
||||
await executeStatementAsync(s,ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function evaluateExpressionAsync(expr,ctx){
|
||||
if(stopRequested) throw new Error("Execution stopped by user");
|
||||
if(expr instanceof FunctionCall){
|
||||
const res = executeFunction(expr.name,ctx.karel);
|
||||
if(['move','turnLeft','pickBeeper','putBeeper'].includes(expr.name)){
|
||||
renderWorld(currentWorld,currentKarel);
|
||||
await sleep(STEP_DELAY);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
throw new Error('Unknown expression');
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 4. WORLD & KAREL
|
||||
**********************************************************************/
|
||||
const DIRECTIONS={NORTH:0,EAST:1,SOUTH:2,WEST:3},
|
||||
DIRECTION_SYMBOLS=['↑','→','↓','←'];
|
||||
class World {
|
||||
constructor(w,h){
|
||||
this.width=w; this.height=h;
|
||||
this.grid=[]; this.walls=[];
|
||||
for(let x=0;x<w;x++){
|
||||
this.grid[x]=[]; this.walls[x]=[];
|
||||
for(let y=0;y<h;y++){
|
||||
this.grid[x][y]=0;
|
||||
this.walls[x][y]={north:false,east:false,south:false,west:false};
|
||||
}
|
||||
}
|
||||
}
|
||||
isWithinBounds(x,y){return x>=0&&x<this.width&&y>=0&&y<this.height;}
|
||||
getBeeperCount(x,y){return this.isWithinBounds(x,y)?this.grid[x][y]:0;}
|
||||
addBeeper(x,y){if(this.isWithinBounds(x,y))this.grid[x][y]++;}
|
||||
removeBeeper(x,y){
|
||||
if(this.isWithinBounds(x,y)&&this.grid[x][y]>0){this.grid[x][y]--;return true;}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
class Karel {
|
||||
constructor(world,x,y,dir){
|
||||
this.world=world;this.x=x;this.y=y;this.direction=dir;this.beeperBag=0;
|
||||
}
|
||||
frontIsClear(){
|
||||
const side=['north','east','south','west'][this.direction];
|
||||
if(this.world.walls[this.x][this.y][side])return false;
|
||||
const [nx,ny]=this.nextPosition(this.direction);
|
||||
return this.world.isWithinBounds(nx,ny);
|
||||
}
|
||||
frontIsBlocked(){return !this.frontIsClear();}
|
||||
leftIsClear(){
|
||||
const d=(this.direction+3)%4,side=['north','east','south','west'][d];
|
||||
if(this.world.walls[this.x][this.y][side])return false;
|
||||
const [nx,ny]=this.nextPosition(d);
|
||||
return this.world.isWithinBounds(nx,ny);
|
||||
}
|
||||
leftIsBlocked(){return !this.leftIsClear();}
|
||||
rightIsClear(){
|
||||
const d=(this.direction+1)%4,side=['north','east','south','west'][d];
|
||||
if(this.world.walls[this.x][this.y][side])return false;
|
||||
const [nx,ny]=this.nextPosition(d);
|
||||
return this.world.isWithinBounds(nx,ny);
|
||||
}
|
||||
rightIsBlocked(){return !this.rightIsClear();}
|
||||
nextToABeeper(){return this.world.getBeeperCount(this.x,this.y)>0;}
|
||||
notNextToABeeper(){return !this.nextToABeeper();}
|
||||
facingNorth(){return this.direction===DIRECTIONS.NORTH;}
|
||||
notFacingNorth(){return !this.facingNorth();}
|
||||
facingSouth(){return this.direction===DIRECTIONS.SOUTH;}
|
||||
notFacingSouth(){return !this.facingSouth();}
|
||||
facingEast(){return this.direction===DIRECTIONS.EAST;}
|
||||
notFacingEast(){return !this.facingEast();}
|
||||
facingWest(){return this.direction===DIRECTIONS.WEST;}
|
||||
notFacingWest(){return !this.facingWest();}
|
||||
anyBeepersInBeeperBag(){return this.beeperBag>0;}
|
||||
noBeepersInBeeperBag(){return this.beeperBag===0;}
|
||||
nextPosition(dir){
|
||||
let nx=this.x,ny=this.y;
|
||||
if(dir===DIRECTIONS.NORTH)ny++;
|
||||
if(dir===DIRECTIONS.EAST) nx++;
|
||||
if(dir===DIRECTIONS.SOUTH)ny--;
|
||||
if(dir===DIRECTIONS.WEST) nx--;
|
||||
return [nx,ny];
|
||||
}
|
||||
move(){
|
||||
if(this.frontIsClear()){
|
||||
[this.x,this.y]=this.nextPosition(this.direction);
|
||||
log(`Moved to (${this.x}, ${this.y})`);
|
||||
} else {
|
||||
log("Cannot move; wall ahead.");
|
||||
}
|
||||
}
|
||||
turnLeft(){
|
||||
this.direction=(this.direction+3)%4;
|
||||
log("Turned left. Now facing "+DIRECTION_SYMBOLS[this.direction]);
|
||||
}
|
||||
pickBeeper(){
|
||||
if(this.world.getBeeperCount(this.x,this.y)>0){
|
||||
this.world.removeBeeper(this.x,this.y);
|
||||
this.beeperBag++;
|
||||
log(`Picked up a beeper. Bag: ${this.beeperBag}`);
|
||||
} else {
|
||||
log("No beeper to pick up.");
|
||||
}
|
||||
}
|
||||
putBeeper(){
|
||||
if(this.beeperBag>0){
|
||||
this.world.addBeeper(this.x,this.y);
|
||||
this.beeperBag--;
|
||||
log(`Placed a beeper. Bag: ${this.beeperBag}`);
|
||||
} else {
|
||||
log("No beeper in bag.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 5. FUNCTION MAPPING
|
||||
**********************************************************************/
|
||||
function executeFunction(name,karel){
|
||||
switch(name){
|
||||
case 'move':return karel.move();
|
||||
case 'turnLeft':return karel.turnLeft();
|
||||
case 'pickBeeper':return karel.pickBeeper();
|
||||
case 'putBeeper':return karel.putBeeper();
|
||||
case 'frontIsClear':return karel.frontIsClear();
|
||||
case 'frontIsBlocked':return karel.frontIsBlocked();
|
||||
case 'leftIsClear':return karel.leftIsClear();
|
||||
case 'leftIsBlocked':return karel.leftIsBlocked();
|
||||
case 'rightIsClear':return karel.rightIsClear();
|
||||
case 'rightIsBlocked':return karel.rightIsBlocked();
|
||||
case 'nextToABeeper':return karel.nextToABeeper();
|
||||
case 'notNextToABeeper':return karel.notNextToABeeper();
|
||||
case 'facingNorth':return karel.facingNorth();
|
||||
case 'notFacingNorth':return karel.notFacingNorth();
|
||||
case 'facingSouth':return karel.facingSouth();
|
||||
case 'notFacingSouth':return karel.notFacingSouth();
|
||||
case 'facingEast':return karel.facingEast();
|
||||
case 'notFacingEast':return karel.notFacingEast();
|
||||
case 'facingWest':return karel.facingWest();
|
||||
case 'notFacingWest':return karel.notFacingWest();
|
||||
case 'anyBeepersInBeeperBag':return karel.anyBeepersInBeeperBag();
|
||||
case 'noBeepersInBeeperBag':return karel.noBeepersInBeeperBag();
|
||||
}
|
||||
throw new Error(`Unknown function: ${name}`);
|
||||
}
|
||||
const context={karel:null,callFunction:n=>executeFunction(n,context.karel)};
|
||||
|
||||
/**********************************************************************
|
||||
* 6. WORLD RENDERING
|
||||
**********************************************************************/
|
||||
function renderWorld(world,karel){
|
||||
currentWorld=world;currentKarel=karel;
|
||||
const w=world.width,h=world.height,total=w*4+1;
|
||||
let out='';
|
||||
// top
|
||||
for(let i=0;i<total;i++){
|
||||
out+= (i===0||i===total-1)
|
||||
? '<span class="map-border">+</span>'
|
||||
: '<span class="map-border">-</span>';
|
||||
}
|
||||
out+='<br>';
|
||||
// rows
|
||||
for(let y=h-1;y>=0;y--){
|
||||
let row='';
|
||||
for(let x=0;x<w;x++){
|
||||
if(x===0) row+='<span class="map-border">|</span>';
|
||||
else if(world.walls[x-1][y].east) row+='<span class="wall-border">|</span>';
|
||||
else row+=' ';
|
||||
let c='.';
|
||||
if(karel.x===x&&karel.y===y) c=DIRECTION_SYMBOLS[karel.direction];
|
||||
else {
|
||||
const bc=world.getBeeperCount(x,y);
|
||||
if(bc>0) c=bc<10?''+bc:'?';
|
||||
}
|
||||
row+=`<span class="cell-content"> ${c} </span>`;
|
||||
}
|
||||
row+='<span class="map-border">|</span><br>';
|
||||
out+=row;
|
||||
if(y>0){
|
||||
let div='';
|
||||
for(let x=0;x<w;x++){
|
||||
if(x===0) div+='<span class="map-border">+</span>';
|
||||
else div+=' ';
|
||||
div+= world.walls[x][y].south
|
||||
? '<span class="wall-border">---</span>'
|
||||
: '<span class="map-border"> </span>';
|
||||
}
|
||||
div+='<span class="map-border">+</span><br>';
|
||||
out+=div;
|
||||
}
|
||||
}
|
||||
// bottom
|
||||
let bot='';
|
||||
for(let i=0;i<total;i++){
|
||||
bot+= (i===0||i===total-1)
|
||||
? '<span class="map-border">+</span>'
|
||||
: '<span class="map-border">-</span>';
|
||||
}
|
||||
bot+='<br>';
|
||||
out+=bot;
|
||||
document.getElementById('map').innerHTML=out;
|
||||
document.getElementById('status').innerHTML=
|
||||
`<div class="status-card"><span class="label">Position:</span><span class="value">(${karel.x}, ${karel.y})</span></div>`+
|
||||
`<div class="status-card"><span class="label">Beepers in Bag:</span><span class="value">${karel.beeperBag}</span></div>`;
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 7. MANUAL MAP EDITOR
|
||||
**********************************************************************/
|
||||
function showMain(){
|
||||
document.getElementById('container').style.display='flex';
|
||||
document.getElementById('mapEditorContainer').style.display='none';
|
||||
}
|
||||
function showEditor(){
|
||||
document.getElementById('container').style.display='none';
|
||||
document.getElementById('mapEditorContainer').style.display='flex';
|
||||
// sync mode
|
||||
document.querySelectorAll('input[name="editMode"]').forEach(radio=>{
|
||||
radio.addEventListener('change',()=>{editMode=radio.value;});
|
||||
});
|
||||
const md = uploadedMapData || {
|
||||
width:3,height:3,
|
||||
walls:[],beepers:[],
|
||||
karelX:0,karelY:0,
|
||||
karelDir:'east',karelBag:0
|
||||
};
|
||||
document.getElementById('editWidth').value=md.width;
|
||||
document.getElementById('editHeight').value=md.height;
|
||||
document.getElementById('karelDirSelect').value=md.karelDir;
|
||||
// beeper bag input
|
||||
const bagInput=document.getElementById('beeperBagInput');
|
||||
bagInput.value=md.karelBag;
|
||||
bagInput.onchange=()=>{
|
||||
let v=parseInt(bagInput.value,10);
|
||||
if(isNaN(v)||v<0)v=0;
|
||||
if(v>99)v=99;
|
||||
bagInput.value=v;
|
||||
editorMapData.karelBag=v;
|
||||
};
|
||||
editorMapData=JSON.parse(JSON.stringify(md));
|
||||
renderEditorMap();
|
||||
}
|
||||
|
||||
function renderEditorMap(){
|
||||
const w=editorMapData.width,h=editorMapData.height,total=w*4+1;
|
||||
let out='';
|
||||
// top border
|
||||
for(let i=0;i<total;i++){
|
||||
const ch=(i===0||i===total-1)?'+':'-';
|
||||
out+=`<span class="map-border editor-border">${ch}</span>`;
|
||||
}
|
||||
out+='<br>';
|
||||
// rows & toggles
|
||||
for(let y=h-1;y>=0;y--){
|
||||
let row='';
|
||||
for(let x=0;x<w;x++){
|
||||
if(x===0){
|
||||
row+='<span class="map-border">|</span>';
|
||||
} else {
|
||||
const hasV=editorMapData.walls.some(wl=>wl.x===x-1&&wl.y===y&&wl.direction==='east');
|
||||
row+=`<span class="clickable-wall ${hasV?'wall-border':'map-border'}"`
|
||||
+` data-x="${x-1}" data-y="${y}" data-dir="east">`
|
||||
+`${hasV?'|':' '}</span>`;
|
||||
}
|
||||
let c='.';
|
||||
const bp=editorMapData.beepers.find(b=>b.x===x&&b.y===y);
|
||||
if(bp)c=bp.count.toString();
|
||||
if(editorMapData.karelX===x&&editorMapData.karelY===y){
|
||||
c={'north':'↑','east':'→','south':'↓','west':'←'}[editorMapData.karelDir];
|
||||
}
|
||||
row+=`<span class="cell-content" data-x="${x}" data-y="${y}"> ${c} </span>`;
|
||||
}
|
||||
row+='<span class="map-border">|</span><br>';
|
||||
out+=row;
|
||||
if(y>0){
|
||||
let div='';
|
||||
for(let x=0;x<w;x++){
|
||||
if(x===0)div+='<span class="map-border">+</span>';
|
||||
else div+=' ';
|
||||
const hasH=editorMapData.walls.some(wl=>wl.x===x&&wl.y===y&&wl.direction==='south');
|
||||
div+=`<span class="clickable-wall ${hasH?'wall-border':'map-border'}"`
|
||||
+` data-x="${x}" data-y="${y}" data-dir="south">`
|
||||
+`${hasH?'---':' '}</span>`;
|
||||
}
|
||||
div+='<span class="map-border">+</span><br>';
|
||||
out+=div;
|
||||
}
|
||||
}
|
||||
// bottom border
|
||||
for(let i=0;i<total;i++){
|
||||
const ch=(i===0||i===total-1)?'+':'-';
|
||||
out+=`<span class="map-border editor-border">${ch}</span>`;
|
||||
}
|
||||
out+='<br>';
|
||||
document.getElementById('editorMap').innerHTML=out;
|
||||
// bind wall toggles
|
||||
document.querySelectorAll('#editorMap .clickable-wall').forEach(el=>{
|
||||
el.onclick=()=>{
|
||||
if(editMode!=='wall')return;
|
||||
const x=+el.dataset.x,y=+el.dataset.y,d=el.dataset.dir;
|
||||
const idx=editorMapData.walls.findIndex(wl=>wl.x===x&&wl.y===y&&wl.direction===d);
|
||||
if(idx>=0)editorMapData.walls.splice(idx,1);
|
||||
else editorMapData.walls.push({x,y,direction:d});
|
||||
renderEditorMap();
|
||||
};
|
||||
});
|
||||
// bind cell clicks
|
||||
document.querySelectorAll('#editorMap .cell-content').forEach(el=>{
|
||||
el.onclick=ev=>{
|
||||
const x=+el.dataset.x,y=+el.dataset.y;
|
||||
if(editMode==='wall'){
|
||||
const d=document.getElementById('karelDirSelect').value;
|
||||
const idx=editorMapData.walls.findIndex(wl=>wl.x===x&&wl.y===y&&wl.direction===d);
|
||||
if(idx>=0)editorMapData.walls.splice(idx,1);
|
||||
else editorMapData.walls.push({x,y,direction:d});
|
||||
} else if(editMode==='beeper'){
|
||||
let b=editorMapData.beepers.find(b=>b.x===x&&b.y===y);
|
||||
if(ev.ctrlKey){
|
||||
if(b){b.count--; if(b.count<=0)editorMapData.beepers=editorMapData.beepers.filter(z=>z!==b);}
|
||||
} else {
|
||||
if(!b)editorMapData.beepers.push({x,y,count:1});
|
||||
else b.count++;
|
||||
}
|
||||
} else if(editMode==='karel'){
|
||||
editorMapData.karelX=x;
|
||||
editorMapData.karelY=y;
|
||||
editorMapData.karelDir=document.getElementById('karelDirSelect').value;
|
||||
}
|
||||
renderEditorMap();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Editor buttons
|
||||
document.getElementById('editMapButton').onclick=showEditor;
|
||||
document.getElementById('discardEditorMapButton').onclick=showMain;
|
||||
document.getElementById('generateMapButton').onclick=()=>{
|
||||
let w=parseInt(document.getElementById('editWidth').value,10),
|
||||
h=parseInt(document.getElementById('editHeight').value,10);
|
||||
if(isNaN(w)||w<3)w=3;
|
||||
if(isNaN(h)||h<3)h=3;
|
||||
document.getElementById('editWidth').value=w;
|
||||
document.getElementById('editHeight').value=h;
|
||||
editorMapData={
|
||||
width:w,height:h,
|
||||
walls:[],beepers:[],
|
||||
karelX:0,karelY:0,
|
||||
karelDir:document.getElementById('karelDirSelect').value,
|
||||
karelBag:0
|
||||
};
|
||||
// reset bag input too
|
||||
document.getElementById('beeperBagInput').value=0;
|
||||
renderEditorMap();
|
||||
};
|
||||
|
||||
// Import from editor
|
||||
document.getElementById('importEditorMapButton').onclick=()=>{
|
||||
uploadedMapData=JSON.parse(JSON.stringify(editorMapData));
|
||||
const md=uploadedMapData, world=new World(md.width,md.height);
|
||||
md.walls.forEach(w=>{
|
||||
world.walls[w.x][w.y][w.direction]=true;
|
||||
if(w.direction==='east'&&w.x+1<world.width)world.walls[w.x+1][w.y].west=true;
|
||||
if(w.direction==='south'&&w.y-1>=0)world.walls[w.x][w.y-1].north=true;
|
||||
});
|
||||
md.beepers.forEach(b=>{for(let i=0;i<b.count;i++)world.addBeeper(b.x,b.y);});
|
||||
const dirNum={north:1,east:2,south:3,west:4}[md.karelDir]-1;
|
||||
const karel=new Karel(world,md.karelX,md.karelY,dirNum);
|
||||
karel.beeperBag=md.karelBag;
|
||||
currentWorld=world;currentKarel=karel;context.karel=karel;
|
||||
renderWorld(world,karel);
|
||||
showMain();
|
||||
};
|
||||
|
||||
// Upload map file
|
||||
document.getElementById('uploadMapButton').onclick=()=>{
|
||||
const inp=document.getElementById('mapFileInput');
|
||||
if(!inp.files.length){alert('Select a file.');return;}
|
||||
const file=inp.files[0],ext=file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
|
||||
if(!['.txt','.kw'].includes(ext)){alert('Only .txt/.kw allowed.');return;}
|
||||
const reader=new FileReader();
|
||||
reader.onload=e=>{
|
||||
try{
|
||||
const md=parseMapFile(e.target.result);
|
||||
uploadedMapData=md;
|
||||
const world=new World(md.width,md.height);
|
||||
md.walls.forEach(w=>{
|
||||
world.walls[w.x][w.y][w.direction]=true;
|
||||
if(w.direction==='east'&&w.x+1<world.width)world.walls[w.x+1][w.y].west=true;
|
||||
if(w.direction==='south'&&w.y-1>=0)world.walls[w.x][w.y-1].north=true;
|
||||
});
|
||||
md.beepers.forEach(b=>{for(let i=0;i<b.count;i++)world.addBeeper(b.x,b.y);});
|
||||
const dirNum={north:1,east:2,south:3,west:4}[md.karelDir]-1;
|
||||
const karel=new Karel(world,md.karelX,md.karelY,dirNum);
|
||||
karel.beeperBag=md.karelBag;
|
||||
currentWorld=world;currentKarel=karel;context.karel=karel;
|
||||
renderWorld(world,karel);
|
||||
log('Map uploaded.');
|
||||
}catch(err){
|
||||
alert('Error parsing map: '+err.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
// Stop button
|
||||
document.getElementById('stopButton').onclick=()=>{
|
||||
stopRequested=true;
|
||||
log('⏹ Execution stopped by user.');
|
||||
};
|
||||
|
||||
/**********************************************************************
|
||||
* 8. COMPILE & RUN
|
||||
**********************************************************************/
|
||||
document.getElementById('runButton').onclick=async()=>{
|
||||
stopRequested=false;
|
||||
document.getElementById('console').textContent='';
|
||||
|
||||
// build world & karel
|
||||
let world,karel;
|
||||
if(uploadedMapData){
|
||||
const md=uploadedMapData;
|
||||
world=new World(md.width,md.height);
|
||||
md.walls.forEach(w=>{
|
||||
world.walls[w.x][w.y][w.direction]=true;
|
||||
if(w.direction==='east'&&w.x+1<world.width)world.walls[w.x+1][w.y].west=true;
|
||||
if(w.direction==='south'&&w.y-1>=0)world.walls[w.x][w.y-1].north=true;
|
||||
});
|
||||
md.beepers.forEach(b=>{for(let i=0;i<b.count;i++)world.addBeeper(b.x,b.y);});
|
||||
const dirNum={north:1,east:2,south:3,west:4}[md.karelDir]-1;
|
||||
karel=new Karel(world,md.karelX,md.karelY,dirNum);
|
||||
karel.beeperBag=md.karelBag;
|
||||
} else {
|
||||
world=new World(10,10);
|
||||
karel=new Karel(world,0,0,DIRECTIONS.EAST);
|
||||
}
|
||||
currentWorld=world;currentKarel=karel;context.karel=karel;
|
||||
renderWorld(world,karel);
|
||||
|
||||
// compile
|
||||
log('=== Compilation ===');
|
||||
let tokens,prog;
|
||||
try{
|
||||
tokens=new Lexer(document.getElementById('editor').value).tokenize();
|
||||
prog=new Parser(tokens).parseProgram();
|
||||
log('✅ Compilation successful.');
|
||||
}catch(e){
|
||||
return log(`⛔ Compilation error: ${e.message}`);
|
||||
}
|
||||
|
||||
// execute
|
||||
log('=== Execution ===');
|
||||
try{
|
||||
await executeProgramAsync(prog,context);
|
||||
if(!stopRequested) log('✅ Execution complete.');
|
||||
}catch(e){
|
||||
if(e.message!=='Execution stopped by user'){
|
||||
log(`⛔ Runtime error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Map-file parser (unchanged) ...
|
||||
function parseMapFile(txt){
|
||||
const lines=txt.split(/\r?\n/).map(l=>l.trim()).filter(l=>l);
|
||||
if(!lines.length)throw new Error('File is empty.');
|
||||
const hdr=lines[0].split(/\s+/);
|
||||
if(hdr.length!==6)throw new Error('Header must have 6 tokens.');
|
||||
const w=parseInt(hdr[0],10),h=parseInt(hdr[1],10),
|
||||
x=parseInt(hdr[2],10)-1,y=parseInt(hdr[3],10)-1,
|
||||
dir=hdr[4].toUpperCase(),bag=parseInt(hdr[5],10);
|
||||
if([w,h,x+1,y+1,bag].some(n=>isNaN(n)))throw new Error('Invalid header.');
|
||||
if(!['N','E','S','W'].includes(dir))throw new Error('Dir must be N/E/S/W.');
|
||||
const mapDir={'N':'north','E':'east','S':'south','W':'west'};
|
||||
const md={width:w,height:h,karelX:x,karelY:y,
|
||||
karelDir:mapDir[dir],karelBag:bag,
|
||||
walls:[],beepers:[]};
|
||||
lines.slice(1).forEach((l,i)=>{
|
||||
const p=l.split(/\s+/),t=p[0].toUpperCase();
|
||||
if(t==='W'){
|
||||
if(p.length!==4)throw new Error(`Line ${i+2}: W needs 4 tokens.`);
|
||||
let xx=parseInt(p[1],10)-1,yy=parseInt(p[2],10)-1,d=p[3].toUpperCase();
|
||||
if(isNaN(xx)||isNaN(yy)||!['N','E','S','W'].includes(d))
|
||||
throw new Error(`Line ${i+2}: invalid wall.`);
|
||||
if(d==='N'&&yy+1<h) md.walls.push({x:xx,y:yy+1,direction:'south'});
|
||||
else if(d==='S') md.walls.push({x:xx,y:yy,direction:'south'});
|
||||
else if(d==='E') md.walls.push({x:xx,y:yy,direction:'east'});
|
||||
else if(d==='W'&&xx-1>=0)md.walls.push({x:xx-1,y:yy,direction:'east'});
|
||||
}
|
||||
else if(t==='B'){
|
||||
if(p.length!==4)throw new Error(`Line ${i+2}: B needs 4 tokens.`);
|
||||
let xx=parseInt(p[1],10)-1,yy=parseInt(p[2],10)-1,
|
||||
c =parseInt(p[3],10);
|
||||
if(isNaN(xx)||isNaN(yy)||isNaN(c))
|
||||
throw new Error(`Line ${i+2}: invalid beeper.`);
|
||||
md.beepers.push({x:xx,y:yy,count:c});
|
||||
}
|
||||
else throw new Error(`Line ${i+2}: must start W or B.`);
|
||||
});
|
||||
return md;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
10
Exam/maintenance/index.html
Normal file
10
Exam/maintenance/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- maintenance/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8"/><title>Down for Maintenance</title></head>
|
||||
<body style="text-align:center;padding:5em;font-family:sans-serif;">
|
||||
<h1>⚙️ Site Under Maintenance</h1>
|
||||
<p>We’ll be back shortly. Thank you for your patience!</p>
|
||||
</body>
|
||||
</html>
|
||||
|
106
Exam/manual.html
Executable file
106
Exam/manual.html
Executable file
@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Karel Simulator — User Manual</title>
|
||||
<style>
|
||||
body {
|
||||
background: #121212;
|
||||
color: #e0e0e0;
|
||||
font-family: monospace;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
h1,h2 { color: #0f0; }
|
||||
a { color: #0f0; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
code { background: #1e1e1e; padding: 2px 4px; border-radius: 3px; }
|
||||
pre { background: #1e1e1e; padding: 10px; border-radius: 4px; overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
|
||||
th, td { border: 1px solid #2a2a2a; padding: 6px; text-align: left; }
|
||||
th { background: #1e1e1e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Karel the Robot – Quick-Start User Guide</h1>
|
||||
|
||||
<h2>1. What Is Karel?</h2>
|
||||
<p>
|
||||
Karel lives in a 2-D grid (“world”) of square cells.
|
||||
He can face North/East/South/West, move one cell at a time,
|
||||
pick up or put down “beepers,” and detect walls.
|
||||
</p>
|
||||
|
||||
<h2>2. Simulator Interface</h2>
|
||||
<ul>
|
||||
<li><strong>Code Editor</strong>: Write your program here.</li>
|
||||
<li><strong>Compile & Run</strong>: Lex, parse, and execute step-by-step.</li>
|
||||
<li><strong>Stop</strong>: Immediately halt execution (for infinite loops).</li>
|
||||
<li><strong>Console</strong>: Logs moves, errors, and debug messages.</li>
|
||||
<li><strong>Upload Map</strong>: Load a <code>.txt</code> or <code>.kw</code> world file.</li>
|
||||
<li><strong>Map View</strong>: ASCII display of walls (red), beepers, and Karel (green).</li>
|
||||
<li><strong>Manual Editor</strong>: Draw your own world, then import it.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Syntax Essentials</h2>
|
||||
<ul>
|
||||
<li>Every statement ends with a semicolon <code>;</code>.</li>
|
||||
<li>Every command or condition <strong>must</strong> have parentheses <code>()</code>.</li>
|
||||
<li>Use <code>if (…){…} [else{…}]</code> and <code>while (…){…}</code>.</li>
|
||||
<li>Comments begin with <code>//</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Built-in Commands</h2>
|
||||
<h3>Movement</h3>
|
||||
<table>
|
||||
<tr><th>Command</th><th>Effect</th></tr>
|
||||
<tr><td><code>move()</code></td><td>Step forward if no wall ahead.</td></tr>
|
||||
<tr><td><code>turnLeft()</code></td><td>Rotate 90° counter-clockwise.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Beeper Handling</h3>
|
||||
<table>
|
||||
<tr><th>Command</th><th>Effect</th></tr>
|
||||
<tr><td><code>pickBeeper()</code></td><td>Pick one beeper from current cell.</td></tr>
|
||||
<tr><td><code>putBeeper()</code></td><td>Place one beeper into current cell.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Sensors & Conditions</h3>
|
||||
<table>
|
||||
<tr><th>Condition</th><th>True When…</th></tr>
|
||||
<tr><td><code>frontIsClear()</code></td><td>No wall immediately in front.</td></tr>
|
||||
<tr><td><code>frontIsBlocked()</code></td><td>Wall immediately in front.</td></tr>
|
||||
<tr><td><code>leftIsClear()</code></td><td>No wall on Karel’s left.</td></tr>
|
||||
<tr><td><code>leftIsBlocked()</code></td><td>Wall on Karel’s left.</td></tr>
|
||||
<tr><td><code>rightIsClear()</code></td><td>No wall on Karel’s right.</td></tr>
|
||||
<tr><td><code>rightIsBlocked()</code></td><td>Wall on Karel’s right.</td></tr>
|
||||
<tr><td><code>nextToABeeper()</code></td><td>One or more beepers in this cell.</td></tr>
|
||||
<tr><td><code>notNextToABeeper()</code></td><td>No beepers in this cell.</td></tr>
|
||||
<tr><td><code>facingNorth()</code>, <code>facingEast()</code>, …</td>
|
||||
<td>Karel’s orientation matches the named direction.</td></tr>
|
||||
<tr><td><code>anyBeepersInBeeperBag()</code></td><td>Bag contains ≥ 1 beeper.</td></tr>
|
||||
<tr><td><code>noBeepersInBeeperBag()</code></td><td>Bag is empty.</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>5. Example</h2>
|
||||
<pre><code>while ( notNextToABeeper() ) {
|
||||
move();
|
||||
}
|
||||
pickBeeper();
|
||||
turnLeft();
|
||||
move();
|
||||
putBeeper();</code></pre>
|
||||
|
||||
<h2>6. Tips</h2>
|
||||
<ul>
|
||||
<li>Sketch Karel’s path on graph paper first.</li>
|
||||
<li>Always check <code>frontIsClear()</code> before <code>move()</code>.</li>
|
||||
<li>Use the <strong>Stop</strong> button to escape accidental infinite loops.</li>
|
||||
<li>Combine <code>if</code> and <code>while</code> for complex behaviors.</li>
|
||||
</ul>
|
||||
|
||||
<p><a href="index.html" style="color:#0f0;">← Back to Simulator</a></p>
|
||||
</body>
|
||||
</html>
|
9
Exam/prepare-app.sh
Executable file
9
Exam/prepare-app.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Name for the Docker image
|
||||
IMAGE_NAME="karel-simulator"
|
||||
|
||||
echo "🔨 Building Docker image '$IMAGE_NAME'..."
|
||||
docker build -t "${IMAGE_NAME}" .
|
||||
echo "✅ Image '${IMAGE_NAME}' built."
|
24
Exam/remove-app.sh
Executable file
24
Exam/remove-app.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
IMAGE_NAME="karel-simulator"
|
||||
CONTAINER_NAME="karel-simulator"
|
||||
|
||||
# Stop & remove the container if it exists
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
|
||||
echo "🗑️ Removing container '${CONTAINER_NAME}'..."
|
||||
docker rm -f "${CONTAINER_NAME}"
|
||||
echo "✅ Container removed."
|
||||
else
|
||||
echo "ℹ️ No container named '${CONTAINER_NAME}' to remove."
|
||||
fi
|
||||
|
||||
# Remove the image if it exists
|
||||
if docker images -q "${IMAGE_NAME}" >/dev/null; then
|
||||
echo "🗑️ Removing image '${IMAGE_NAME}'..."
|
||||
docker rmi "${IMAGE_NAME}"
|
||||
echo "✅ Image removed."
|
||||
else
|
||||
echo "ℹ️ No image named '${IMAGE_NAME}' to remove."
|
||||
fi
|
||||
|
20
Exam/start-app.sh
Executable file
20
Exam/start-app.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# 1) Clean up any existing containers (manual or compose)
|
||||
for NAME in karel-simulator karel-redis; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then
|
||||
echo "🛑 Removing existing container '${NAME}'..."
|
||||
docker rm -f "${NAME}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
# 2) (Re)start both services in detached mode
|
||||
echo "🚀 Starting multi-container application..."
|
||||
docker compose up -d
|
||||
|
||||
# 3) Status
|
||||
echo "✅ All containers are up:"
|
||||
docker ps --filter "name=karel" --format " • {{.Names}} ({{.Image}}) → {{.Ports}}"
|
||||
echo ""
|
||||
echo "🌐 Simulator: http://localhost:8080/"
|
5
Exam/stop-app.sh
Executable file
5
Exam/stop-app.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
echo "⏹ Shutting down all containers..."
|
||||
docker-compose down
|
||||
echo "✅ Stopped."
|
64
assignment1/README.md
Normal file
64
assignment1/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Docker Web Application Deployment
|
||||
|
||||
The application consists of two services:
|
||||
|
||||
1. **Web Service (Nginx):**
|
||||
- Uses the official Nginx image.
|
||||
- Listens on port 80 inside the container and is mapped to host port 8080.
|
||||
2. **Database Service (MySQL 5.7):**
|
||||
- Uses the official MySQL 5.7 image.
|
||||
- Configured with a root password and a default database.
|
||||
- Persists its data using the named volume `mysql-data`.
|
||||
|
||||
## Files Overview
|
||||
|
||||
- **prepare-app.sh:**
|
||||
- Creates the required Docker network (`myapp-net`) and persistent volume (`mysql-data`).
|
||||
- **docker-compose.yaml:**
|
||||
- Defines the two services along with their ports, environment variables, volumes, and restart policies.
|
||||
- **start-app.sh:**
|
||||
- Starts the services using Docker Compose in detached mode.
|
||||
- Displays the URL to access the web service.
|
||||
- **stop-app.sh:**
|
||||
- Stops the running containers without deleting the persistent volume.
|
||||
- **remove-app.sh:**
|
||||
- Removes all the created resources (containers, network, and volume) from the deployment.
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
1. **Preparation:**
|
||||
- Ensure Docker and Docker Compose are installed.
|
||||
- Make the scripts executable:
|
||||
```bash
|
||||
chmod +x prepare-app.sh start-app.sh stop-app.sh remove-app.sh
|
||||
```
|
||||
- Run the preparation script:
|
||||
```bash
|
||||
./prepare-app.sh
|
||||
```
|
||||
|
||||
2. **Starting the Application:**
|
||||
- Launch the services by running:
|
||||
```bash
|
||||
./start-app.sh
|
||||
```
|
||||
- Open your web browser and navigate to [http://localhost:8080](http://localhost:8080) to see the Nginx welcome page.
|
||||
|
||||
3. **Stopping the Application:**
|
||||
- Stop the services without losing data:
|
||||
```bash
|
||||
./stop-app.sh
|
||||
```
|
||||
|
||||
4. **Removing the Application:**
|
||||
- To completely remove all deployed resources, run:
|
||||
```bash
|
||||
./remove-app.sh
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The application uses an external network (`myapp-net`) and a persistent volume (`mysql-data`) that are created in `prepare-app.sh`.
|
||||
- The Nginx container depends on the MySQL container to demonstrate inter-service communication within the `myapp-net` network.
|
||||
- Containers are configured to restart on failure.
|
||||
|
32
assignment1/docker-compose.yaml
Normal file
32
assignment1/docker-compose.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "8080:80"
|
||||
networks:
|
||||
- myapp-net
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: mysql:5.7
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: example
|
||||
MYSQL_DATABASE: appdb
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
networks:
|
||||
- myapp-net
|
||||
restart: on-failure
|
||||
|
||||
networks:
|
||||
myapp-net:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
external: true
|
||||
|
20
assignment1/prepare-app.sh
Executable file
20
assignment1/prepare-app.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# prepare-app.sh
|
||||
|
||||
if ! docker network ls | grep -w myapp-net >/dev/null; then
|
||||
docker network create myapp-net
|
||||
echo "Created Docker network: myapp-net"
|
||||
else
|
||||
echo "Docker network myapp-net already exists."
|
||||
fi
|
||||
|
||||
|
||||
if ! docker volume ls | grep -w mysql-data >/dev/null; then
|
||||
docker volume create mysql-data
|
||||
echo "Created Docker volume: mysql-data"
|
||||
else
|
||||
echo "Docker volume mysql-data already exists."
|
||||
fi
|
||||
|
||||
echo "Preparation complete."
|
||||
|
10
assignment1/remove-app.sh
Executable file
10
assignment1/remove-app.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# remove-app.sh
|
||||
|
||||
docker-compose down
|
||||
|
||||
docker volume rm mysql-data
|
||||
docker network rm myapp-net
|
||||
|
||||
echo "All application resources removed."
|
||||
|
9
assignment1/start-app.sh
Executable file
9
assignment1/start-app.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# start-app.sh
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
sleep 5
|
||||
|
||||
echo "Application started. Access the web service at http://localhost:8080"
|
||||
|
6
assignment1/stop-app.sh
Executable file
6
assignment1/stop-app.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# stop-app.sh
|
||||
|
||||
docker-compose stop
|
||||
echo "Application services stopped. Persistent data remains intact."
|
||||
|
BIN
assignment2/.prepare-app.sh.swp
Normal file
BIN
assignment2/.prepare-app.sh.swp
Normal file
Binary file not shown.
2
assignment2/Dockerfile
Normal file
2
assignment2/Dockerfile
Normal file
@ -0,0 +1,2 @@
|
||||
FROM nginx:alpine
|
||||
COPY weather.html /usr/share/nginx/html/index.html
|
6
assignment2/Dockerfile.api
Normal file
6
assignment2/Dockerfile.api
Normal file
@ -0,0 +1,6 @@
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY server.js .
|
||||
RUN npm init -y && npm install express pg body-parser
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
170
assignment2/README.md
Normal file
170
assignment2/README.md
Normal file
@ -0,0 +1,170 @@
|
||||
# Weather Web App - Kubernetes Deployment
|
||||
|
||||
This project deploys a full-stack weather web application to Kubernetes. It includes:
|
||||
- A frontend website to search weather by city
|
||||
- A backend PostgreSQL database for logging searches
|
||||
- A Node.js API to connect the frontend to the database
|
||||
|
||||
---
|
||||
|
||||
## Application Description
|
||||
|
||||
- **Frontend (weather.html):** A responsive, modern web UI that shows current weather data using the OpenWeatherMap API.
|
||||
- **Backend (PostgreSQL):** Stores weather search history via a `weather_log` table.
|
||||
- **API Server (Node.js):** Receives weather data from the frontend and logs it to the PostgreSQL database.
|
||||
|
||||
---
|
||||
|
||||
## Containers Used
|
||||
|
||||
| Container | Image | Description |
|
||||
|------------------|--------------------|----------------------------------------------|
|
||||
| `weather-frontend` | Custom Nginx-based | Serves the `weather.html` UI |
|
||||
| `postgres` | `postgres:15` | Provides relational database backend |
|
||||
| `weather-api` | Custom Node.js | API that logs weather data to PostgreSQL |
|
||||
|
||||
---
|
||||
|
||||
## ☸️ Kubernetes Objects
|
||||
|
||||
| Object Type | File | Description |
|
||||
|------------------------|------------------------|----------------------------------------------------------------------|
|
||||
| `Namespace` | (inside script) | Isolates resources under `webapp-ns` |
|
||||
| `Deployment` | `deployment.yaml` | Manages frontend app |
|
||||
| `Deployment` | `deployment-api.yaml` | Manages the Node.js API |
|
||||
| `StatefulSet` | `statefulset.yaml` | Manages PostgreSQL instance with persistent volume |
|
||||
| `PersistentVolume` | `statefulset.yaml` | Host-mounted volume for PostgreSQL data |
|
||||
| `PersistentVolumeClaim`| `statefulset.yaml` | Requests storage for StatefulSet |
|
||||
| `Service` | `service.yaml` | Exposes frontend via NodePort |
|
||||
| `Service` | `deployment-api.yaml` | Exposes API via NodePort |
|
||||
| `Service` | `postgres-service.yaml`| Internal ClusterIP service for PostgreSQL DNS resolution |
|
||||
| `ConfigMap` | `db-init-script` | Stores the SQL init script for table creation |
|
||||
|
||||
---
|
||||
|
||||
## Networking & Volumes
|
||||
|
||||
### Virtual Networks:
|
||||
- Kubernetes services provide internal DNS for `postgres` and `weather-api-service`.
|
||||
- Frontend and API exposed externally using `NodePort` services.
|
||||
|
||||
### Volumes:
|
||||
- `PersistentVolume` and `PersistentVolumeClaim` ensure PostgreSQL data is retained.
|
||||
- `ConfigMap` used to apply `init-db.sql` manually via `psql`.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Container Configurations
|
||||
|
||||
### Frontend
|
||||
- Dockerfile uses `nginx:alpine`
|
||||
- Serves `weather.html`
|
||||
- Exposed on port `80`
|
||||
|
||||
### Node.js API
|
||||
- Built with Node.js 18
|
||||
- Listens on port `3000`
|
||||
- Connects to PostgreSQL using service DNS `postgres`
|
||||
- Accepts POST requests at `/log`
|
||||
|
||||
### PostgreSQL
|
||||
- `postgres:15` with `weatheruser`, `weatherpass`, and `weatherdb`
|
||||
- Table `weather_log` is created manually after StatefulSet starts
|
||||
|
||||
---
|
||||
|
||||
## How to Use the Application
|
||||
|
||||
### 1. Build Docker Images
|
||||
|
||||
```bash
|
||||
./prepare-app.sh
|
||||
```
|
||||
|
||||
### 2. Start the App (create namespace and apply all configs)
|
||||
|
||||
```bash
|
||||
./start-app.sh
|
||||
```
|
||||
|
||||
### 3. Manually Initialize the Database
|
||||
|
||||
```bash
|
||||
kubectl exec -it -n webapp-ns statefulset/postgres -- psql -U weatheruser -d weatherdb
|
||||
```
|
||||
|
||||
Paste this inside the prompt:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS weather_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
city VARCHAR(100),
|
||||
temperature DECIMAL(5,2),
|
||||
description TEXT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
Exit with:
|
||||
```sql
|
||||
\q
|
||||
```
|
||||
|
||||
### 4. View the App
|
||||
|
||||
```bash
|
||||
minikube service weather-service -n webapp-ns
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
minikube ip
|
||||
kubectl get svc -n webapp-ns
|
||||
```
|
||||
Then open:
|
||||
```
|
||||
http://<minikube-ip>:<frontend-nodeport>
|
||||
```
|
||||
|
||||
### 5. Use the App
|
||||
- Search for a city
|
||||
- Weather appears
|
||||
- Weather data is logged via API to PostgreSQL
|
||||
|
||||
### 6. View Logged Data
|
||||
|
||||
```bash
|
||||
kubectl exec -it -n webapp-ns statefulset/postgres -- psql -U weatheruser -d weatherdb
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT * FROM weather_log ORDER BY timestamp DESC;
|
||||
```
|
||||
|
||||
### 7. Stop and Clean Up
|
||||
|
||||
```bash
|
||||
./stop-app.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Key Setup
|
||||
|
||||
- Register at [OpenWeatherMap](https://openweathermap.org/api)
|
||||
- Replace the key in `weather.html`:
|
||||
|
||||
```js
|
||||
const apiKey = "YOUR_API_KEY";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
- Frontend fetches real-time weather from OpenWeatherMap
|
||||
- Logs are POSTed to backend and stored in PostgreSQL
|
||||
|
||||
---
|
||||
|
41
assignment2/deployment-api.yaml
Normal file
41
assignment2/deployment-api.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: weather-api
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: weather-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: weather-api
|
||||
spec:
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: busybox
|
||||
command: ['sh', '-c', 'until nslookup postgres; do echo waiting for db; sleep 2; done']
|
||||
containers:
|
||||
- name: weather-api
|
||||
image: weather-api:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: weather-api-service
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
selector:
|
||||
app: weather-api
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
nodePort: 31000
|
||||
type: NodePort
|
||||
|
22
assignment2/deployment.yaml
Normal file
22
assignment2/deployment.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: weather-frontend
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: weather-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: weather-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: weather-frontend
|
||||
image: weather-frontend:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
11
assignment2/init-db.sql
Normal file
11
assignment2/init-db.sql
Normal file
@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS weather_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
city VARCHAR(100),
|
||||
temperature DECIMAL(5,2),
|
||||
description TEXT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO weather_log (city, temperature, description)
|
||||
VALUES ('London', 15.5, 'partly cloudy');
|
||||
|
0
assignment2/inti-db.sql
Normal file
0
assignment2/inti-db.sql
Normal file
BIN
assignment2/minikube-linux-amd64
Normal file
BIN
assignment2/minikube-linux-amd64
Normal file
Binary file not shown.
13
assignment2/postgres-service.yaml
Normal file
13
assignment2/postgres-service.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
|
11
assignment2/prepare-app.sh
Executable file
11
assignment2/prepare-app.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
IMAGE_NAME=weather-frontend
|
||||
IMAGE_TAG=lates
|
||||
|
||||
echo "🔧 Building Docker image: $IMAGE_NAME:$IMAGE_TAG"
|
||||
docker build -t $IMAGE_NAME:$IMAGE_TAG .
|
||||
docker build -t weather-api:latest -f Dockerfile.api .
|
||||
|
||||
echo "✅ Build complete."
|
||||
|
35
assignment2/server.js
Normal file
35
assignment2/server.js
Normal file
@ -0,0 +1,35 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const { Pool } = require('pg');
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
const pool = new Pool({
|
||||
host: 'postgres',
|
||||
user: 'weatheruser',
|
||||
password: 'weatherpass',
|
||||
database: 'weatherdb',
|
||||
port: 5432
|
||||
});
|
||||
|
||||
app.post('/log', async (req, res) => {
|
||||
const { city, temperature, description } = req.body;
|
||||
try {
|
||||
await pool.query(
|
||||
'INSERT INTO weather_log (city, temperature, description) VALUES ($1, $2, $3)',
|
||||
[city, temperature, description]
|
||||
);
|
||||
res.status(200).send('Logged!');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Error saving data');
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Weather API listening on port ${port}`);
|
||||
});
|
||||
|
14
assignment2/service.yaml
Normal file
14
assignment2/service.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: weather-service
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
selector:
|
||||
app: weather-frontend
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: NodePort
|
||||
|
17
assignment2/start-app.sh
Executable file
17
assignment2/start-app.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Creating namespace 'webapp-ns'..."
|
||||
kubectl create namespace webapp-ns
|
||||
|
||||
echo "📦 Deploying frontend app..."
|
||||
kubectl apply -f deployment.yaml
|
||||
kubectl apply -f service.yaml
|
||||
|
||||
echo "🧾 Creating ConfigMap with init-db.sql..."
|
||||
kubectl create configmap db-init-script --from-file=init-db.sql=init-db.sql -n webapp-ns
|
||||
|
||||
echo "🗄️ Deploying database backend..."
|
||||
kubectl apply -f statefulset.yaml
|
||||
kubectl apply -f deployment-api.yaml
|
||||
|
||||
echo "✅ All resources created."
|
65
assignment2/statefulset.yaml
Normal file
65
assignment2/statefulset.yaml
Normal file
@ -0,0 +1,65 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: postgres-pv
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: "/mnt/data/postgres"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: webapp-ns
|
||||
spec:
|
||||
serviceName: "postgres"
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:15
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: "weatheruser"
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "weatherpass"
|
||||
- name: POSTGRES_DB
|
||||
value: "weatherdb"
|
||||
volumeMounts:
|
||||
- name: postgres-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: postgres-storage
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
7
assignment2/stop-app.sh
Executable file
7
assignment2/stop-app.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🧼 Deleting all resources in namespace 'webapp-ns'..."
|
||||
kubectl delete namespace webapp-ns
|
||||
|
||||
echo "✅ Cleanup complete."
|
||||
|
122
assignment2/weather.html
Normal file
122
assignment2/weather.html
Normal file
@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Weather Info</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(to right, #e0f7fa, #80deea);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #00796b;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.5rem;
|
||||
width: 65%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.55rem 1rem;
|
||||
background-color: #00796b;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #00695c;
|
||||
}
|
||||
|
||||
#weather {
|
||||
margin-top: 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#weather p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🌦️ Check the Weather</h1>
|
||||
<input type="text" id="city" placeholder="Enter city" />
|
||||
<button onclick="getWeather()">Get Weather</button>
|
||||
<div id="weather"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function getWeather() {
|
||||
const city = document.getElementById("city").value;
|
||||
const apiKey = "0ef7f5ac4207c6da232b7843eb1a663e";
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.cod === 200) {
|
||||
document.getElementById("weather").innerHTML = `
|
||||
<h2>${data.name}, ${data.sys.country}</h2>
|
||||
<p><strong>${data.weather[0].description}</strong></p>
|
||||
<p>🌡️ <strong>${data.main.temp}°C</strong></p>
|
||||
`;
|
||||
|
||||
// Log to backend
|
||||
await fetch("http://192.168.49.2:31000/log", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
city: data.name,
|
||||
temperature: data.main.temp,
|
||||
description: data.weather[0].description
|
||||
})
|
||||
});
|
||||
|
||||
} else {
|
||||
document.getElementById("weather").innerHTML = `<p style="color:red;">City not found!</p>`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fetch failed:", err);
|
||||
document.getElementById("weather").innerHTML = `<p style="color:red;">Error getting weather data.</p>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user