Read the original article:Building Hangman with ArkTS
Introduction
Let's travel back in time to my childhood and merge it with today's tech.
What you will learn:
- Getting text input with input filters and a character limit.
- Using a Button to fire an action.
- Using Canvas to draw graphics.
- Creating custom popups.
Let's not wait anymore.
We will use TextInput with an input filter to prevent numbers and special characters, and a character limit to only accept one letter at a time.
// define controller and text parameter
tcn: TextInputController = new TextInputController()
txt: string = ''
// add TextInput
TextInput({ controller: this.tcn })
.maxLength(1)
.inputFilter('^[a-zA-Z]*$')
.onChange((txt: string) => {
this.txt = txt
})
To submit the entered character, we will use a Button.
Button('Submit')
.onClick(() => {
if (this.txt.length === 1) {
// guess
}
})
Let's add some style; this part only depends on your imagination.
@Entry
@Component
struct Index {
tcn: TextInputController = new TextInputController()
txt: string = ''
build() {
Column() {
Blank().layoutWeight(1) // canvas will be here
Row() {
TextInput({ controller: this.tcn })
.maxLength(1)
.inputFilter('^[a-zA-Z]*$')
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.borderRadius({
topRight: 0,
bottomRight: 0,
topLeft: 10,
bottomLeft: 10
})
.onChange((txt: string) => {
this.txt = txt
})
Button('Submit')
.height(40)
.layoutWeight(1)
.borderRadius({
topRight: 10,
bottomRight: 10,
topLeft: 0,
bottomLeft: 0
})
.onClick(() => {
if (this.txt.length === 1) {
// guess
}
})
}
.width('70%')
.padding({ bottom: '15%' })
}
.height('100%')
.width('100%')
}
}
The result should look like this:
Let's create our view model and add the Canvas. Create Hangman.ets under the viewmodel directory.
Here we will implement data and canvas operations.
@Observed
export default class Hangman {
// Initialize Canvas context
context: CanvasRenderingContext2D = new CanvasRenderingContext2D()
}
Modify Index.ets; Initialize Hangman and add the Canvas.
// initialize Hangman view model
@State hangman: Hangman = new Hangman()
// replace Blank with the Canvas
Canvas(this.hangman.context)
.backgroundColor(Color.Grey) // we will remove this later
.layoutWeight(1)
The Result:
Finally, we need the secret word and the characters we found. We can easily do this by storing empty strings in an array and changing them with real values as the user guesses.
Modify Hangman.ets;
// define current value array
current: string[] = ['','','',''] // we will remove this initial value later
Modify Index.ets;
Row({ space: 8 }) {
Blank().layoutWeight(1)
ForEach(this.hangman.current, (letter: string) => {
Stack() {
Text(letter)
.height(18)
}
.width(8)
.border({
color: Color.White,
width: {
left: 0,
right: 0,
top: 0,
bottom: 1
},
})
})
Blank().layoutWeight(1)
}
.width('70%')
.padding({ top: 8, bottom: 8 })
The result:
Let's implement the logic. First, we will define the necessary models and parameters.
Create HangmanStatus.ets and CanvasPoint.ets under the model directory.
- HangmanStatus.ets
export enum HangmanStatus {
playing,
win,
lose
}
- CanvasPoint.ets
export interface CanvasPoint {
x: number
y: number
}
Now we can work on our view model, Hangman.ets.
Let's start by defining some secret words. These can be stored anywhere you want. For simplicity, we will put it in the same file.
const questions = [
'huawei',
'harmonyos',
'turkey',
'istanbul',
'black',
'lion',
'computer',
'engineer'
]
Define necessary parameters.
preguesses: string[] = [] // this will store previous wrong guesses
private goal: string = '' // this will store the secret value
status: HangmanStatus = HangmanStatus.playing // current game status
And the initializer to start the game.
constructor() {
this.start()
}
start() {
this.context.reset() // clear the canvas
this.goal = questions[Math.round(Math.random() * (questions.length - 1))]
this.status = HangmanStatus.playing
this.preguesses = []
this.current = new Array(this.goal.length).fill('')
}
Time to guess. We will get one word from the user and show all occurrences if the secret word contains it, or continue drawing the stick figure.
guess(letter: string) {
if (this.status !== HangmanStatus.playing) {
return
}
let newCurrent = [...this.current]
if (this.goal.includes(letter)) {
for (let i = 0; i < this.goal.length; i++) {
if (letter === this.goal.charAt(i)) {
newCurrent[i] = letter
}
}
this.current = newCurrent
} else if (!this.preguesses.includes(letter)) {
this.preguesses.push(letter)
this.draw(this.preguesses.length) // this will show an error
}
this.checkGame() // this will show an error
}
Let's draw. (This part is just some math, so we will not dive into details)
private draw(step: number) {
this.context.lineWidth = 2
this.context.strokeStyle = "#FFFFFF"
const l = 20
const canvasWidth = this.context.width
const canvasHeight = this.context.height
// where vertical line starts
const bottomx = canvasWidth / 2 - l / 2
const bottomy = canvasHeight - 8
// base
if (step === 1) {
const start: CanvasPoint = { x: bottomx - l, y: bottomy }
const end: CanvasPoint = { x: bottomx + 2 * l, y: bottomy }
this.drawLine(start, end)
}
// vertical line
if (step === 2) {
const start: CanvasPoint = { x: bottomx, y: bottomy }
const end: CanvasPoint = { x: bottomx, y: bottomy - 3 * l }
this.drawLine(start, end)
}
// top horizontal line
if (step === 3) {
const start: CanvasPoint = { x: bottomx, y: bottomy - 3 * l }
const end: CanvasPoint = { x: bottomx + 2 * l, y: bottomy - 3 * l }
this.drawLine(start, end)
}
// rope
if (step === 4) {
const start: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - 3 * l }
const end: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - 2.5 * l }
this.drawLine(start, end)
}
// head
if (step === 5) {
this.context.beginPath()
this.context.arc(bottomx + 1.5 * l, bottomy - 2 * l, l / 2, 0, 6.28)
this.context.stroke()
}
// neck
if (step === 6) {
const start: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - 1.5 * l }
const end: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - 1.25 * l }
this.drawLine(start, end)
}
// arms
if (step === 7) {
const start: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - 1.25 * l }
const end1: CanvasPoint = { x: bottomx + l, y: bottomy - .75 * l }
const end2: CanvasPoint = { x: bottomx + 2 * l, y: bottomy - .75 * l }
this.drawLine(start, end1)
this.drawLine(start, end2)
}
// body
if (step === 8) {
const start: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - 1.25 * l }
const end: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - .75 * l }
this.drawLine(start, end)
}
// legs
if (step === 9) {
const start: CanvasPoint = { x: bottomx + 1.5 * l, y: bottomy - .75 * l }
const end1: CanvasPoint = { x: bottomx + l, y: bottomy - .25 * l }
const end2: CanvasPoint = { x: bottomx + 2 * l, y: bottomy - .25 * l }
this.drawLine(start, end1)
this.drawLine(start, end2)
}
}
// helper function to draw lines
private drawLine(from: CanvasPoint, to: CanvasPoint) {
this.context.beginPath()
this.context.moveTo(from.x, from.y)
this.context.lineTo(to.x, to.y)
this.context.stroke()
}
And the game checker.
private checkGame() {
if (this.preguesses.length >= 9) {
this.status = HangmanStatus.lose
} else if (this.current.join('') === this.goal) {
this.status = HangmanStatus.win
}
}
Now we can call the guess function. Modify the submit button.
.onClick(() => {
if (this.txt.length === 1) {
this.hangman.guess(this.txt)
this.tcn.deleteText()
}
})
You should be able to make guesses and see the stick figure.
Do not give up, we are one step away from perfection.
We need to inform the user when the game ends. Let's create a custom pop-up to show the game result.
Create WinLosePopup.ets under the components directory.
@Component
export default struct WinLosePopup {
@Link showPopup: boolean;
@Require win: boolean;
build() {
Column() {
Text(`YOU ${this.win ? 'WON' : 'LOST'}`)
.fontColor(this.win ? Color.Green : Color.Red)
Blank().size({ height: 8 })
Button() {
SymbolGlyph($r('sys.symbol.arrow_clockwise'))
.fontColor([Color.White])
}
.size({
width: '100%',
height: 32
})
.onClick(() => {
this.showPopup = false
})
}
.width('70%')
.backgroundColor(Color.White)
.padding(16)
}
}
We will call it from Index.ets.
First, define a state parameter:
@State pop: boolean = false;
And the builder:
@Builder
winLosePopupBuilder() {
WinLosePopup({
showPopup: this.pop,
win: this.hangman.status == HangmanStatus.win
})
}
Now bind the popup:
build() {
Column() {
...
}
.bindPopup(this.pop, {
builder: this.winLosePopupBuilder(),
autoCancel: false,
onStateChange: (e) => {
if (!e.isVisible) {
this.hangman.start()
}
},
})
}
Of course, this will not open with magic, but something like it. We will watch our view model and open the pop-up when the game state changes.
@State @Watch('winLose') hangman: Hangman = new Hangman()
winLose() {
this.pop = this.hangman.status != HangmanStatus.playing
}
Do not forget to remove the grey background color from the Canvas.
The Final Result:
Conclusion
We did it! Hope you had fun. See you all in new adventures. :)
~ Fortuna Favet Fortibus
Top comments (0)