Eksamen: REA3049 Informasjonsteknologi 2 | Semester: Vår 2026 | Varighet: 5 timer | Språkvariant: C# | Tema: OOP-begreper, pseudokode og flytskjema, togsett-modellering, kjøretøydatasett, etikk (kameraovervåking), spillprogrammering
Begrunnelse: En konstruktør er en spesiell metode som kjøres automatisk når et nytt objekt (en instans) opprettes fra en klasse. Oppgaven dens er å initialisere objektets tilstand – altså gi attributtene sine startverdier. I C# har konstruktøren samme navn som klassen og har ingen returtype, og den mottar eventuelle argumenter:
class Elev
{
public string Navn { get; set; }
public int Alder { get; set; }
public Elev(string navn, int alder)
{
Navn = navn; // startverdi for attributt
Alder = alder; // startverdi for attributt
}
}
Elev elev = new Elev("Noah", 15); // konstruktøren kjøres automatisk her
De andre alternativene beskriver andre mekanismer: å definere metodene gjøres i klassekroppen generelt; å opprette nye klasser basert på eksisterende er arv; og å styre kommunikasjon mellom objekter handler om meldingsutveksling/metodekall, ikke om initialisering.
Kjøretøy, og Bil og Buss er spesialiseringer av Kjøretøy. Hvilket utsagn beskriver best hvordan arv fungerer?
Begrunnelse: Arv (generalisering/spesialisering) går fra superklasse til subklasse. Kjøretøy er superklassen (det generelle), mens Bil og Buss er subklasser (det spesialiserte). Subklassene arver alt som er definert i superklassen, og kan i tillegg legge til egne attributter og metoder eller overstyre arvede metoder.
class Kjoretoy
{
public int Hastighet { get; set; }
public Kjoretoy(int hastighet)
{
Hastighet = hastighet;
}
}
class Bil : Kjoretoy // arver fra Kjøretøy
{
public int AntallDorer { get; set; }
public Bil(int hastighet, int antallDorer) : base(hastighet) // arvet attributt
{
AntallDorer = antallDorer; // egen attributt
}
}
Alternativet «Kjøretøy får tilgang til alt i Bil og Buss» snur arveretningen feil vei (superklassen kjenner ikke subklassene). At klassene skulle være «uavhengige» er også feil – de deler en generaliseringsrelasjon.
SET antall TO 0SET i TO 1WHILE i <= 5READ tallIF tall > 10SET antall TO antall + 1ENDIFSET i TO i + 1ENDWHILEDISPLAY antallBegrunnelse: Vi trenger to variabler: en teller antall som starter på 0, og en løkketeller i som styrer at vi leser nøyaktig fem tall. Inne i løkka leses ett tall, det testes mot 10, telleren økes ved treff, og løkketelleren økes til slutt. Legg merke til at SET antall TO antall + 1 må ligge inne i IF … ENDIF, mens SET i TO i + 1 må ligge utenfor IF men inne i WHILE – ellers telles feil.
Tilsvarende C#-kode ser slik ut:
int antall = 0;
int i = 1;
while (i <= 5)
{
int tall = int.Parse(Console.ReadLine());
if (tall > 10)
antall = antall + 1;
i = i + 1;
}
Console.WriteLine(antall);
Slik løser du 4a: Les flytskjemaet ovenfra og ned. Noter for hver blokk hvilken type den er:
READ/DISPLAY)IF/WHILE) – én pil ut for «ja», én for «nei»Gå deretter gjennom hver av de tre pseudokode-sekvensene og sjekk at rekkefølgen på handlinger, den samme betingelsen (f.eks. > mot >=), og løkkestrukturen (om testen står før eller etter kroppen) stemmer overens med flytskjemaet. Sekvensen som har identisk rekkefølge og identiske betingelser, er svaret. Vær særlig oppmerksom på forskjellen mellom en WHILE-løkke (test først) og en REPEAT/UNTIL-løkke (test sist), og på om en pil peker tilbake til før eller etter en tilordning.
Algoritmen starter med å sette en teller til null. Deretter går den inn i en løkke som gjentas så lenge betingelsen i romben er oppfylt. For hver runde leser den inn en verdi fra brukeren, sammenligner verdien med en fast grense i beslutningsblokken, og – dersom betingelsen er sann – utfører den en handling (for eksempel å øke telleren eller oppdatere en sum). Når betingelsen ikke lenger er oppfylt, hopper algoritmen ut av løkka og skriver ut resultatet før den avslutter. Kort sagt: algoritmen leser inn data gjentatte ganger, tar en avgjørelse for hver verdi, akkumulerer et resultat, og presenterer resultatet til slutt.
Et godt 4b-svar navngir blokktypene, beskriver løkkas start- og sluttbetingelse, forklarer hva beslutningsblokken tester, og oppsummerer algoritmens formål i én setning – akkurat som malen over.
Togsett skal automatisk få et Lokomotiv (5a–b). Lokomotiver instansieres med tilfeldig trekkraft fra [320, 450, 600] tonn. I 5c innføres Togdel og Vogn: vogner får tilfeldig vekt 20–50 tonn, togdeler har unikt serienummer, og det skal gå an å legge til både nye og eksisterende vogner. I 5d legges vogner til helt til lokomotivets trekkraft er nådd.
Togdel er en superklasse med unikt serienummer og vekt, mens Lokomotiv og Vogn arver fra den (figur 2). Togsett «har-et» lokomotiv og «har-mange» vogner (komposisjon). Lokomotivets egenvekt er ikke oppgitt og teller ikke mot trekkraft-grensen – trekkraften tolkes som maks samlet vognvekt.
Vi bruker komposisjon: når et Togsett opprettes, oppretter konstruktøren automatisk sitt eget Lokomotiv. Lokomotivet velger tilfeldig trekkraft med en delt Random (Random.Shared).
// Togsett.cs — oppgave 5
using System;
class Lokomotiv
{
private static readonly int[] TrekkraftValg = { 320, 450, 600 }; // tonn
public int Trekkraft { get; }
public Lokomotiv()
{
Trekkraft = TrekkraftValg[Random.Shared.Next(TrekkraftValg.Length)];
}
public override string ToString()
{
return $"Lokomotiv med trekkraft {Trekkraft} tonn";
}
}
class Togsett
{
public Lokomotiv Lokomotiv { get; }
public Togsett()
{
// Komposisjon: et togsett FÅR automatisk et lokomotiv
Lokomotiv = new Lokomotiv();
}
}
class Program
{
static void Main()
{
Togsett togsett = new Togsett();
Console.WriteLine("Nytt togsett opprettet.");
Console.WriteLine(togsett.Lokomotiv); // f.eks. "Lokomotiv med trekkraft 450 tonn"
}
}
Nå innføres superklassen Togdel. Den holder et statisk felt som teller opp slik at hvert serienummer blir unikt. Lokomotiv og Vogn arver fra Togdel. Togsett får metoder for å legge til både nye og eksisterende vogner.
// Togsett.cs — full versjon etter 5c
using System;
using System.Collections.Generic;
using System.Linq;
// Superklasse for alle deler i et togsett. Gir unikt serienummer.
class Togdel
{
private static int nesteSerienummer = 1;
public int Serienummer { get; }
public int Vekt { get; }
public Togdel(int vekt)
{
Serienummer = nesteSerienummer;
nesteSerienummer++; // garanterer unikhet
Vekt = vekt;
}
public override string ToString()
{
return $"Togdel #{Serienummer} ({Vekt} tonn)";
}
}
class Lokomotiv : Togdel
{
private static readonly int[] TrekkraftValg = { 320, 450, 600 }; // tonn
public int Trekkraft { get; }
public Lokomotiv() : base(120) // egenvekt (ikke spesifisert; teller ikke mot grensen)
{
Trekkraft = TrekkraftValg[Random.Shared.Next(TrekkraftValg.Length)];
}
public override string ToString()
{
return $"Lokomotiv #{Serienummer}: trekkraft {Trekkraft} tonn";
}
}
class Vogn : Togdel
{
public Vogn() : base(Random.Shared.Next(20, 51)) // tilfeldig 20–50 tonn
{
}
public override string ToString()
{
return $"Vogn #{Serienummer}: vekt {Vekt} tonn";
}
}
class Togsett
{
public Lokomotiv Lokomotiv { get; }
public List<Vogn> Vogner { get; }
public Togsett()
{
Lokomotiv = new Lokomotiv(); // komposisjon
Vogner = new List<Vogn>();
}
public int SamletVognvekt()
{
return Vogner.Sum(v => v.Vekt);
}
// Sant hvis vognen får plass innenfor trekkraften.
public bool KanLeggeTil(Vogn vogn)
{
return SamletVognvekt() + vogn.Vekt <= Lokomotiv.Trekkraft;
}
// Legg til en eksisterende vogn, eller opprett en ny hvis ingen gis.
public Vogn LeggTilVogn(Vogn vogn = null)
{
vogn ??= new Vogn(); // ny vogn hvis ingen gis
Vogner.Add(vogn); // eksisterende ELLER ny
return vogn;
}
}
class Program
{
static void Main()
{
Togsett togsett = new Togsett();
Console.WriteLine("Nytt togsett opprettet.");
Console.WriteLine(togsett.Lokomotiv);
// Legg til vogner helt til neste vogn ikke får plass innenfor trekkraften
while (true)
{
Vogn vogn = new Vogn();
if (!togsett.KanLeggeTil(vogn))
break;
togsett.LeggTilVogn(vogn);
}
Console.WriteLine(
$"\nLa til {togsett.Vogner.Count} vogner " +
$"(samlet vekt {togsett.SamletVognvekt()} av maks " +
$"{togsett.Lokomotiv.Trekkraft} tonn):");
foreach (Vogn vogn in togsett.Vogner)
{
Console.WriteLine($" {vogn}");
}
}
}
Nytt togsett opprettet.Lokomotiv #1: trekkraft 450 tonnLa til 12 vogner (samlet vekt 431 av maks 450 tonn): Vogn #2: vekt 34 tonn Vogn #3: vekt 41 tonn … (osv.)
Innledning. Å filme all undervisning og aktivitet i kantina reiser et grunnleggende etisk dilemma: hensynet til et tryggere og bedre læringsmiljø står mot elevenes rett til personvern og frihet. Jeg vil drøfte dette dilemmaet med utgangspunkt i Noahs situasjon.
Argumenter for overvåking. Skolen begrunner kameraene med at de kan redusere konflikter, hærverk og bråk, og gi lærerne innsikt i arbeidsro og samarbeid. I kantina kan kameraer virke avskrekkende og gjøre det lettere å oppklare hendelser i etterkant, slik at elever som Noah kan føle seg tryggere i friminuttene. Nytteargumentet er altså at overvåkingen kan gi en gevinst for fellesskapet: færre konflikter og et roligere miljø for de mange.
Argumenter mot overvåking. Samtidig opplever Noah det som ubehagelig at alt han gjør i timen blir filmet – særlig når han spør om hjelp eller er usikker. Her ligger kjernen i dilemmaet: konstant overvåking kan gjøre at elever slutter å tørre å prøve og feile. Læring krever nettopp at man våger å stille «dumme» spørsmål og gjøre feil. Når kameraet alltid ser på, kan det oppstå en nedkjølingseffekt der elevene endrer atferd for å unngå å bli vurdert – ikke fordi de lærer bedre, men fordi de føler seg overvåket. Dette går ut over både trivsel og reell læring.
Personvern og samtykke. Videoopptak av navngitte, mindreårige elever er sensitive personopplysninger. Etter personvernregelverket (GDPR/personopplysningsloven) må formålet være tydelig, nødvendig og forholdsmessig, og det må finnes et gyldig behandlingsgrunnlag. Når en ekstern teknologibedrift analyserer opptakene, blir spørsmålene skjerpet: Hvem eier dataene? Hvor lenge lagres de? Kan de brukes til å trene analysemodeller som selges videre? At elevene er 15 år gjør kravet til reelt, informert samtykke – og til foreldrenes involvering – enda viktigere.
Forholdsmessighet. Et sentralt etisk prinsipp er at inngrepet må stå i forhold til gevinsten. Kanskje er kameraer i kantina lettere å forsvare (åpent fellesareal, dokumentert hærverk) enn kameraer som filmer hver enkelt elev gjennom hele undervisningen. Mindre inngripende tiltak – flere voksne til stede, bedre klasseledelse, anonymiserte observasjoner – bør vurderes før man tyr til totalovervåking. Noahs egen usikkerhet på om overvåkingen «faktisk hjelper, eller bare fører til stress» peker nettopp på at gevinsten er usikker, mens ulempen (tap av frihet) er konkret.
Konklusjon. Dilemmaet lar seg ikke løse ved å velge bort det ene hensynet helt. En etisk forsvarlig løsning krever at overvåkingen begrenses til det som er strengt nødvendig og forholdsmessig, at formål og datalagring er åpne og tidsavgrensede, at elever og foresatte gis reell medbestemmelse, og at man aktivt måler om tiltaket faktisk virker. Uten disse rammene veier tapet av trygghet, frihet og læringsvilje tyngre enn den usikre gevinsten – og da bør elevenes personvern gå foran.
aar;region;drivstofftype;antall (semikolon-separert, UTF-8). Er formatet «bredt» eller JSON, endrer du bare innlesningen i LesData() – resten av logikken er uendret. Elbiler gjenkjennes på drivstofftypen «Elektrisk».
// Kjoretoy.cs — oppgave 7
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
// Én rad fra datasettet.
class Kjoretoyrad
{
public int Aar { get; set; }
public string Region { get; set; }
public string Drivstofftype { get; set; }
public int Antall { get; set; }
}
class Program
{
const string Datafil = "kjoretoy_2008_2024.csv";
const string El = "elektrisk"; // drivstofftype for elbil (sammenlignes med ToLower())
// Les CSV og returner liste av rader: Aar, Region, Drivstofftype, Antall.
static List<Kjoretoyrad> LesData(string filsti = Datafil)
{
var rader = new List<Kjoretoyrad>();
string[] linjer = File.ReadAllLines(filsti, Encoding.UTF8);
foreach (string linje in linjer.Skip(1)) // hopp over kolonneoverskriftene
{
if (string.IsNullOrWhiteSpace(linje)) continue;
string[] felt = linje.Split(';');
rader.Add(new Kjoretoyrad
{
Aar = int.Parse(felt[0]),
Region = felt[1],
Drivstofftype = felt[2],
Antall = int.Parse(felt[3])
});
}
return rader;
}
// Sum av elbiler per år på tvers av alle regioner.
static Dictionary<int, int> ElbilerPerAar(List<Kjoretoyrad> data)
{
return data
.Where(r => r.Drivstofftype.ToLower() == El)
.GroupBy(r => r.Aar)
.ToDictionary(g => g.Key, g => g.Sum(r => r.Antall));
}
static void VisElbiltabell(List<Kjoretoyrad> data, int fra = 2014, int til = 2024)
{
var perAar = ElbilerPerAar(data);
Console.WriteLine($"\n{"År",-6}{"Elbiler",12}");
Console.WriteLine(new string('-', 18));
for (int aar = fra; aar <= til; aar++)
{
int antall = perAar.GetValueOrDefault(aar, 0);
Console.WriteLine($"{aar,-6}{antall,12:n0}");
}
}
// For gitt år: region -> (elbiler, totalt).
static Dictionary<string, (int Elbiler, int Totalt)> RegionStatistikk(List<Kjoretoyrad> data, int aar)
{
var stat = new Dictionary<string, (int Elbiler, int Totalt)>();
foreach (var rad in data.Where(r => r.Aar == aar))
{
stat.TryGetValue(rad.Region, out var verdi);
verdi.Totalt += rad.Antall;
if (rad.Drivstofftype.ToLower() == El)
verdi.Elbiler += rad.Antall;
stat[rad.Region] = verdi;
}
return stat;
}
static void VisAar(List<Kjoretoyrad> data, int aar)
{
var stat = RegionStatistikk(data, aar);
// Sorter synkende etter antall elbiler
var rader = stat.OrderByDescending(kv => kv.Value.Elbiler);
Console.WriteLine($"\nElbiler per region i {aar} (synkende):");
Console.WriteLine($"{"Region",-20}{"Elbiler",10}{"Andel",10}");
Console.WriteLine(new string('-', 40));
foreach (var (region, tall) in rader)
{
double andel = tall.Totalt != 0 ? (double)tall.Elbiler / tall.Totalt * 100 : 0;
Console.WriteLine($"{region,-20}{tall.Elbiler,10}{andel,9:F1} %");
}
}
Datasettet plottes uten eksterne pakker: vi tegner selv et linjediagram på et Form-vindu med Graphics.DrawLines. Én linje per drivstofftype.
// Linjediagram for valgt region, én linje per drivstofftype (Windows Forms).
static void Linjediagram(List<Kjoretoyrad> data, string region)
{
var iRegion = data.Where(r => r.Region.ToLower() == region.ToLower()).ToList();
if (iRegion.Count == 0)
{
Console.WriteLine($"Fant ingen data for regionen '{region}'.");
return;
}
// drivstofftype -> (aar -> antall)
var perType = iRegion
.GroupBy(r => r.Drivstofftype)
.OrderBy(g => g.Key)
.ToDictionary(
g => g.Key,
g => g.GroupBy(r => r.Aar).ToDictionary(a => a.Key, a => a.Sum(r => r.Antall)));
int[] aarListe = data.Select(r => r.Aar).Distinct().OrderBy(a => a).ToArray();
Application.EnableVisualStyles();
Application.Run(new Diagramskjema(region, aarListe, perType));
}
[STAThread]
static void Main()
{
var data = LesData();
VisElbiltabell(data); // 7a
Console.Write("\nVelg et år (2014–2024): ");
int aar = int.Parse(Console.ReadLine());
VisAar(data, aar); // 7b
Console.Write("Velg en region (f.eks. Telemark): ");
string region = Console.ReadLine().Trim();
Linjediagram(data, region); // 7c
}
}
// Egendefinert tegneflate — linjediagram uten eksterne pakker.
class Diagramskjema : Form
{
private readonly string region;
private readonly int[] aarListe;
private readonly Dictionary<string, Dictionary<int, int>> perType;
private static readonly Color[] Paletten =
{
Color.SteelBlue, Color.SeaGreen, Color.IndianRed,
Color.Goldenrod, Color.MediumPurple, Color.Teal
};
public Diagramskjema(string region, int[] aarListe,
Dictionary<string, Dictionary<int, int>> perType)
{
this.region = region;
this.aarListe = aarListe;
this.perType = perType;
Text = $"Registrerte kjøretøy i {region} (2008–2024)";
ClientSize = new Size(900, 600);
BackColor = Color.White;
DoubleBuffered = true;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
int margV = 70, margH = 70;
int b = ClientSize.Width - margV - margH;
int h = ClientSize.Height - 2 * margH;
int x0 = margV, y0 = ClientSize.Height - margH;
int maks = perType.Values.SelectMany(d => d.Values).DefaultIfEmpty(1).Max();
if (maks == 0) maks = 1;
// Akser
g.DrawLine(Pens.Black, x0, y0, x0 + b, y0); // x-akse
g.DrawLine(Pens.Black, x0, y0, x0, y0 - h); // y-akse
using var liten = new Font("Consolas", 8);
// Årstall langs x-aksen
for (int i = 0; i < aarListe.Length; i++)
{
float x = x0 + b * (aarListe.Length == 1 ? 0 : (float)i / (aarListe.Length - 1));
g.DrawString(aarListe[i].ToString(), liten, Brushes.Black, x - 12, y0 + 6);
}
// Én linje per drivstofftype
int fargeNr = 0, legendeY = margH;
foreach (var (drivstoff, verdier) in perType)
{
Color farge = Paletten[fargeNr % Paletten.Length];
using var penn = new Pen(farge, 2);
using var punktpensel = new SolidBrush(farge);
var punkter = new List<PointF>();
for (int i = 0; i < aarListe.Length; i++)
{
verdier.TryGetValue(aarListe[i], out int antall);
float x = x0 + b * (aarListe.Length == 1 ? 0 : (float)i / (aarListe.Length - 1));
float y = y0 - h * ((float)antall / maks);
punkter.Add(new PointF(x, y));
}
if (punkter.Count >= 2)
g.DrawLines(penn, punkter.ToArray());
foreach (var p in punkter)
g.FillEllipse(punktpensel, p.X - 3, p.Y - 3, 6, 6);
// Forklaring (tegnforklaring)
g.FillRectangle(punktpensel, x0 + b - 150, legendeY, 12, 12);
g.DrawString(drivstoff, liten, Brushes.Black, x0 + b - 132, legendeY - 2);
legendeY += 18;
fargeNr++;
}
// Titler
using var stor = new Font("Segoe UI", 11, FontStyle.Bold);
g.DrawString($"Registrerte kjøretøy i {region}", stor, Brushes.Black, x0, 20);
g.DrawString("Antall registrerte", liten, Brushes.Black, 6, margH - 24);
g.DrawString("År", liten, Brushes.Black, x0 + b / 2, y0 + 28);
}
}
System.Windows.Forms/System.Drawing), som følger med .NET og lar oss lage enkle 2D-spill uten eksterne pakker. Siden delene alltid er 50 px høye, blir de 50 px brede etter kvartrotasjonen, og stolpene stables ryddig oppover. En stolpe regnes som ferdig når den samlede høyden når 400 px (toppen av scenen). Piltastene styrer byggeren. Kjør med dotnet run (krever .NET SDK).
// Stolpebyggeren.cs — oppgave 8
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
static class Konst
{
public const int Bredde = 800, Hoyde = 400;
public const int FangerB = 150, FangerH = 70;
public const int DelH = 50;
public static readonly int[] DelBredder = { 50, 100, 150 };
public const int Kant = 50; // deler minst 50 px fra sidene
public const int StolpeB = DelH; // bredde på en rotert del
public static readonly Dictionary<string, Color> Farger = new()
{
["blå"] = Color.FromArgb(33, 150, 243),
["grønn"] = Color.FromArgb(76, 175, 80)
};
public const int FartFanger = 9;
public const int FartFall = 3;
public const int SpawnMs = 1300;
}
class Stolpedel
{
public Rectangle Rect;
public string Fargenavn { get; }
public Stolpedel(int x, int bredde, string fargenavn)
{
Rect = new Rectangle(x, -Konst.DelH, bredde, Konst.DelH);
Fargenavn = fargenavn;
}
public void Fall()
{
Rect.Y += Konst.FartFall;
}
public void Roter()
{
// Kvart rotasjon (90°): bredde og høyde bytter plass
int gammelBredde = Rect.Width;
Rect.Width = Rect.Height;
Rect.Height = gammelBredde;
}
public void Tegn(Graphics g)
{
using var fyll = new SolidBrush(Konst.Farger[Fargenavn]);
using var kant = new Pen(Color.FromArgb(30, 30, 30), 2);
g.FillRectangle(fyll, Rect);
g.DrawRectangle(kant, Rect);
}
}
// En sidestolpe som bygges nedenfra og opp med vekslende farger.
class Stolpe
{
private readonly int x;
public List<Stolpedel> Deler { get; } = new();
public Stolpe(int xVenstre)
{
x = xVenstre;
}
public int Hoyde()
{
return Deler.Sum(d => d.Rect.Height);
}
public string NesteFarge()
{
if (Deler.Count == 0)
return "blå"; // første del kan være hvilken som helst; vi krever blå
return Deler[^1].Fargenavn == "blå" ? "grønn" : "blå";
}
public bool Ferdig()
{
return Hoyde() >= Konst.Hoyde;
}
public void LeggTil(Stolpedel del)
{
del.Roter(); // blir StolpeB bred, gammel bredde blir høyde
del.Rect.Width = Konst.StolpeB;
del.Rect.X = x;
del.Rect.Y = Konst.Hoyde - Hoyde() - del.Rect.Height;
Deler.Add(del);
}
public void Tegn(Graphics g)
{
foreach (var d in Deler)
d.Tegn(g);
}
}
class Stolpebygger
{
public Rectangle Rect;
public int Liv { get; set; } = 3;
public Stolpebygger()
{
Rect = new Rectangle((Konst.Bredde - Konst.FangerB) / 2,
Konst.Hoyde - Konst.FangerH, Konst.FangerB, Konst.FangerH);
}
public void Flytt(int dx)
{
Rect.X = Math.Max(0, Math.Min(Konst.Bredde - Konst.FangerB, Rect.X + dx));
}
public void Tegn(Graphics g)
{
using var fyll = new SolidBrush(Color.FromArgb(90, 90, 90));
using var kant = new Pen(Color.FromArgb(20, 20, 20), 3);
g.FillRectangle(fyll, Rect);
g.DrawRectangle(kant, Rect);
}
}
class Spill : Form
{
private readonly Stolpebygger bygger = new();
private readonly List<Stolpedel> fallende = new();
private readonly Stolpe hoyre = new(Konst.Bredde - Konst.StolpeB); // bygges først, helt til høyre
private readonly Stolpe venstre = new(0); // bygges sist, helt til venstre
private readonly Timer klokke = new();
private readonly Font font = new("Consolas", 12);
private int sistSpawn = 0;
private bool venstreTast = false, hoyreTast = false;
public Spill()
{
Text = "Stolpebyggeren";
ClientSize = new Size(Konst.Bredde, Konst.Hoyde);
BackColor = Color.FromArgb(240, 240, 245);
DoubleBuffered = true;
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false;
klokke.Interval = 1000 / 60; // ~60 bilder i sekundet
klokke.Tick += (s, e) => Steg();
klokke.Start();
}
private Stolpe AktivStolpe()
{
return !hoyre.Ferdig() ? hoyre : venstre;
}
private void SpawnDel()
{
for (int forsok = 0; forsok < 20; forsok++) // prøv å finne ledig posisjon uten overlapp
{
int bredde = Konst.DelBredder[Random.Shared.Next(Konst.DelBredder.Length)];
int x = Random.Shared.Next(Konst.Kant, Konst.Bredde - Konst.Kant - bredde + 1);
var ny = new Rectangle(x, -Konst.DelH, bredde, Konst.DelH);
if (!fallende.Any(d => d.Rect.IntersectsWith(ny)))
{
string farge = Konst.Farger.Keys.ElementAt(Random.Shared.Next(Konst.Farger.Count));
fallende.Add(new Stolpedel(x, bredde, farge));
return;
}
}
}
private void Steg()
{
// Styring
if (venstreTast) bygger.Flytt(-Konst.FartFanger);
if (hoyreTast) bygger.Flytt(Konst.FartFanger);
// Spawn nye deler med jevne mellomrom
sistSpawn += klokke.Interval;
if (sistSpawn >= Konst.SpawnMs)
{
SpawnDel();
sistSpawn = 0;
}
// Fall + kollisjon
foreach (var del in fallende.ToList())
{
del.Fall();
if (del.Rect.IntersectsWith(bygger.Rect))
{
Fang(del);
fallende.Remove(del);
}
else if (del.Rect.Top > Konst.Hoyde)
{
fallende.Remove(del); // bommet — ingen straff
}
}
if ((hoyre.Ferdig() && venstre.Ferdig()) || bygger.Liv <= 0)
{
klokke.Stop();
VisResultat();
return;
}
Invalidate(); // be om ny opptegning (OnPaint)
}
private void Fang(Stolpedel del)
{
Stolpe stolpe = AktivStolpe();
if (del.Fargenavn == stolpe.NesteFarge())
stolpe.LeggTil(del); // riktig farge
else
bygger.Liv--; // feil farge koster et liv
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
hoyre.Tegn(g);
venstre.Tegn(g);
foreach (var d in fallende)
d.Tegn(g);
bygger.Tegn(g);
string info = $"Liv: {bygger.Liv} Neste farge: {AktivStolpe().NesteFarge()}";
g.DrawString(info, font, Brushes.Black, 10, 10);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyCode == Keys.Left) venstreTast = true;
if (e.KeyCode == Keys.Right) hoyreTast = true;
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (e.KeyCode == Keys.Left) venstreTast = false;
if (e.KeyCode == Keys.Right) hoyreTast = false;
}
private void VisResultat()
{
string tittel = bygger.Liv > 0
? "Gratulerer! Begge stolpene er ferdig bygd."
: "Game over – du mistet alle livene.";
string melding =
$"{tittel}\n\n" +
$"Deler i høyre stolpe: {hoyre.Deler.Count}\n" +
$"Deler i venstre stolpe: {venstre.Deler.Count}";
MessageBox.Show(melding, "Stolpebyggeren");
Close();
}
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new Spill());
}
}
Stolpedel (én del), Stolpe (en sidestolpe under bygging), Stolpebygger (spilleren) og Spill (spill-løkke og tilstand).Stolpe.NesteFarge() avgjør hvilken farge som må fanges neste gang; feil farge håndteres i Fang() med tap av liv.AktivStolpe() bygger høyre stolpe først og går over til venstre når høyre er ferdig.SpawnDel() prøver på nytt til den finner en posisjon som ikke kolliderer med deler som allerede faller.Foreslått mappestruktur (f.eks. 123456.zip):
123456/
├── oppgave4b/
│ └── forklaring_algoritme.txt (tekstsvaret ditt til 4b)
├── oppgave5/
│ └── Togsett.cs
├── oppgave7/
│ ├── Kjoretoy.cs
│ └── kjoretoy_2008_2024.csv (datasettet)
├── oppgave8/
│ └── Stolpebyggeren.cs
└── README.txt (hvordan hver fil kjøres, og hva som må være installert)
README.txt forklarer hvordan hver fil kjøres (dotnet run) og at .NET SDK må være installert (oppgave 7 og 8 bruker Windows Forms, som følger med .NET på Windows)Om oppgaveteksten: Oppgaveteksten i dette løsningsforslaget er gjengitt fra Utdanningsdirektoratets (UDIR) eksamen i Informasjonsteknologi IT 2 – C# (våren 2026). Vi gjengir oppgaveteksten bevisst, slik at du kan følge løsningen uten å veksle mellom dokumenter. Eksamensoppgaver fra offentlige myndigheter er uten opphavsrettsvern etter åndsverkloven § 14 og kan gjengis fritt. Selve løsningsforslaget, forklaringene og figurene er utarbeidet av Eksamenssett.no. Opphavsrettsbeskyttede bilder og illustrasjoner fra originaleksamen er fjernet.