- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Guided: C# 14 Delegates, Events, and Lambdas
This C# 14 CodeLab incorporates the use of features such as delegates, lambda functions, and event-driven behavior. You will work on a small .NET console application that has been scaffolded for you, building upon usage of delegates as contracts for certain behaviors and lambda functions to supply said behaviors. By the end of this lab, you will have defined a publisher-subscriber abstraction wherein you connect components by subscribing to event-driven outcomes.
Lab Info
Table of Contents
-
Challenge
Introduction
Welcome to the Guided: C# 14 Delegates, Events, and Lambdas CodeLab.
In this lab, you will complete a single-player console game where you choose
Move.CooperateorMove.Defectand a computer opponent answers with a strategy callback. You have already been provided with a game runner inSoloRoundGame/ConsoleGameRunner.cs, the domain types inRoundContext,RoundResult, andRoundResolvedEventArgs, and the scaffolded members that shape each task you will complete (labelled asTODOin the comments). You will fill in the missing logic for round resolution, callbacks, events, subscriber updates, and formatting.You will use several callable "shapes" during the exercise. You use
ComputerMoveSelectorwhen you want a named delegate with game-specific meaning: upcoming round number in, computer move out. You useAction<RoundResult>when you want notification without a return value. You useFunc<RoundContext, Move>andFunc<RoundResult, string>when you want a value back from a callback. You useEventHandler<RoundResolvedEventArgs>when you want the engine to publish a completed round and let several subscribers react without giving subscribers control over round resolution.The game uses a fixed payoff table that is already described in
RoundEngine: when both sides cooperate, both score3; when both sides defect, both score1; when only one side defects, the defector scores5and the cooperator scores0. The steps below build outward from that rule set, starting with a single resolved round and then adding callbacks, lambdas, events, subscribers, and final result formatting.
Feel free to refer to the
solutiondirectory if you are stuck or to compare your implementations.The validation tests should also output their results to help you troubleshoot, but if for some reason the output gets obscured or isn't specific enough, you can manually run a test in the Terminal with
./runTest taskX. ReplaceXwith the task number, which are numbered1through9. -
Challenge
Build the round engine core
You will work in
SoloRoundGame/Engine/RoundEngine.csfor this step.
You have been provided with constructs such as
RoundContextwithRoundNumber,HumanMove, andComputerMove, as well as aRoundResultwithContext,HumanPayoff,ComputerPayoff, andSummary. You will populate those values insidePlayRoundand keep round creation in one place so later overloads and callbacks reuse the same logic.You are also given
ResolvePayoffs,BuildSummary, andDescribeMoveas helpers inRoundEngine.cs. You should use those helpers to keep thePlayRoundmethods focused on orchestration: build context, choose moves, create results, and trigger hooks. This step gives you the central source of truth for one completed round, which prevents repeated game-state recomputation across later callbacks and subscribers.
You use a delegate when you want to pass behavior as data.
ComputerMoveSelectorgives that idea a domain name that fits the game. You usePostRoundCallbackas anAction<RoundResult>because you only need post-round side effects and you would useComputerStrategyOverrideasFunc<RoundContext, Move>because you need a callback that can inspect round data and then return a move.To start with
- check the Trust the authors... box, then click Yes, I trust the authors.
- You can also close the right-hand CHAT panel.
At this point, you should have one engine path that resolves a round directly, one engine path that delegates computer choice to
ComputerMoveSelector, and two callback hooks that later steps can build on. -
Challenge
Write strategy lambdas
You will work in
SoloRoundGame/Strategies/ComputerStrategies.csfor this step.
You use a lambda when you want short behavior without creating an extra named method. You use an expression lambda for a one-expression rule, a statement lambda for a block body, and a
staticlambda when you want to guarantee zero capture from surrounding locals or fields.You already get
Func<RoundContext, Move>as the shape for every strategy. You can ignoreRoundContextin simple strategies by using_, but you still keep the same delegate shape soConsoleGameRunnercan plug every strategy intoComputerStrategyOverride. This is why lambdas are useful in this case: you can swap computer behavior without changing engine code or adding branching logic insideRoundEngine. You should now have three interchangeable strategy callbacks that all share one delegate shape but express behavior in different lambda styles. -
Challenge
Publish events and handle subscribers
You will work across
RoundEngine.cs,RoundResolvedEventArgs.cs,Scoreboard.cs,HistoryLog.cs, andAnalyticsObserver.csfor this step.
You use an event when you want one publisher to notify multiple listeners about one completed action. The existing events are
RoundResolvedEventArgs.Resultfor round data andRoundResolvedEventArgs.StopRequestedfor a small control signal that can flow back to the engine.You also have three subscriber classes with state that survives across rounds.
Scoreboardholds cumulative totals,HistoryLogstores summary strings, andAnalyticsObservertracks mutual defections. You do not need unsubscribe logic inside any subscriber. You only need handler methods that update local state correctly when calling code that subscribes or unsubscribes those handlers.By using these events, the game engine can publish one round result, and several other classes can react without taking over round resolution. By this point, you now have one publisher that emits round results, three subscribers that react to those results, and one stop-request path that flows from subscriber back to the game engine.
-
Challenge
Finish the formatting callback
Lastly, you will need to setup a formatting callback to display the summary of each round through
SoloRoundGame/Analysis/RoundAnalysis.cs.
You use
Func<RoundResult, string>when you want reusable formatting behavior instead of immediate formatting. TheRoundResult.Summaryalready exists, so you only need one callback that prefixes a caller-supplied label. You now have one final callback that lets calling code add display context without changing the core round data.
If you have reached this point, then the game should now be complete. You have built a reusable round engine that resolves game state, swaps computer behavior through delegates and lambdas, publishes updates through events, and lets separate classes handle scorekeeping, history, analytics, and formatting without collapsing those concerns into one class.
In the TERMINAL, run the game to observe the behaviors you implemented with
dotnet run --project SoloRoundGame. Play around with it, and feel free to make modifications to it if you wish.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.