How to Build a Hunt the Wumpus Game Using Python, Flask, and Socket.io
In this tutorial, we will create a simple app based on the Hunt the Wumpus game. This was one of the first computer games. <!--more--> By reading this article, the reader will learn about the history of Hunt the Wumpus.
Individuals will also understand how to build a clone of it from scratch, using Flask and Socket.io in Python.
Table of contents
- Introduction
- Prerequisites
- A brief history of the game and its contributions to AI
- What is Socket.io and how does it work?
- Project structure
- The content of
index.html
- Building the backend
- Interacting with the game using JavaScript and Socket.io
- Conclusion
- Further reading
Introduction
Hunt the Wumpus, developed in 1973, was among the most popular games during the introduction of the personal computers.
In this game, the player is an adventurer who enters a dungeon looking for a treasure. The goal is to find the treasure and get out of the dungeon by avoiding the pitfalls and the Wumpus (a monster).
To do that, the player has to pay attention to the signals. When he is near a pit, he can feel the breeze, and when he is near the Wumpus, he can smell the Wumpus' stink.
Hunt the Wumpus was studied by computer scientists in the early days of AI technology. In this tutorial, we will build a simple graphical web-based "Hunt the Wumpus" game using Flask and Socket.io in Python.
Prerequisites
To follow this tutorial, you should know:
- Basic aspects of HTML and CSS.
- How to interact with elements of a web page using JavaScript and jQuery.
- The basics of Python programming and Flask.
A brief history of the game and its contributions to AI
Hunt the Wumpus is a text-based adventure game developed in 1973 by Gregory Yob for PC.
In the game, the player enters a dungeon made of several connected rooms and has to avoid pitfalls, bats that can move him to a random room, and kill the monster Wumpus with an arrow.
The player will know if there's a pit nearby when he feels a breeze and he knows when there's a Wumpus nearby when he feels a stink.
The "Wumpus World" is a slightly modified version of the Hunt the Wumpus game as mentioned in one of the most influential books: Artificial Intelligence: a Modern Approach.
In the book, it is described as:
"The wumpus world is a cave consisting of rooms connected by passageways. Lurking somewhere in the cave is the terrible wumpus, a beast that eats anyone who enters its room. The wumpus can be shot by an agent, but the agent has only one arrow. Some rooms contain bottomless pits that will trap anyone who wanders into these rooms (except for the wumpus, which is too big to fall in). The only mitigating feature of this bleak environment is the possibility of finding a heap of gold." (Artificial Intelligence: A Modern Approach)
This Wumpus World exemplifies the use of propositional logic in AI to build knowledge-based agents.
The book also shows how one can build an agent that can play Wumpus World "rationally", that is, by choosing the safest rooms based on the available knowledge.
In this tutorial, we will develop a game similar to Wumpus World. However, instead of building a text-based game, we will focus on a graphical version of the game.
Furthermore, instead of killing the Wumpus, we set the goal of the player only to find the treasure.
What is Socket.io and how does it work?
Socket.io is a library that allows us to build apps with bidirectional communication between the server and client using an event-based approach.
With Socket.io, we can emit events with messages and hear the events and messages. This way we can create communication between a server and a client.
One side emits events and sends a message. The other side receives the message with some data and then processes it.
Here, we use Socket.io because we can't reveal information about the dungeons to the player on the client-side. Only when a player enters a room he will know what's around.
Here is what the flow would like:
- We will store the dungeon's information on the server.
- When the player makes a move on the client-side, we emit events asking for information about the new player's position.
- The server listens to the client, processes the player's position, and sends another event with the required information about the player's surroundings.
- The client then displays the information to the player.
Project structure
This project's structure is similar to any other Flask project. First, we create a directory app
for the project and add a file wsgi.py
.
Now, add a new file app.py
along with two other directories static
and templates
.
Under the templates/
directory, add two files - base.html
and index.html
. In the static/
directory, create two more directories - scripts
and styles
.
Next, create a main.js
file under scripts/
, and main.css
under styles/
.
It's almost done! Your directory structure should look like this:
| wsgi.py
|
+---app
| | app.py
| |
| +---static
| | +---scripts
| | | main.js
| | |
| | \---styles
| | main.css
| |
| +---templates
| | base.html
| | index.html
Create a virtual environment using python -m venv myenv
and activate it. Then, install the following packages:
- Flask:
python -m pip install Flask
- Flask-SocketIO:
python -m pip install Flask-SocketIO
- Flask-Session:
python -m pip install Flask-Session
Now, let's define the initial visuals of the app inside the base.html
and main.css
by adding the page's meta information. We will create the header and footer in the base.html
.
We also add the Google fonts that link to main.css
and main.js
, as well as the CDNs to Socket.io and jQuery.
Feel free to create the visuals of the header and the footer as you want, but don't forget to add the links and CDNs.
Between the header and the footer, you should also add the following code: {% block content %}{% endblock %}
. This is where we are going to put the page content:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hunt the Wumpus</title>
<meta charset="utf-8"/>
<meta name="description" content="In Hunt the Wumpus, the player is an adventurer entering a dungeon looking for a treasure. The goal of the adventurer is to find the treasure and get out of the dungeon avoiding the pits where he can fall in and the Wumpus: a monster who kills anyone it sees.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--Fonts from Google Fonts-->
<!--Eczar-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Eczar:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!--CSS-->
<link rel= "stylesheet" type= "text/css" href= "{{ url_for('static',filename='styles/main.css') }}">
<!--JavaScript-->
<!--Socketio-->
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js" integrity="sha384-fKnu0iswBIqkjxrhQCTZ7qlLHOFEgNkRmK2vaO/LbTZSXdJfAu6ewRBdwHPhBo/H" crossorigin="anonymous"></script>
<!--jQuery-->
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<!--Feather Icons-->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<!--main.js-->
<script src="{{ url_for('static', filename='scripts/main.js') }}"></script>
</head>
<body>
<div class='header'>
<div class='title'>
<a href='#'><h2>Hunt the Wumpus</h2></a>
</div>
</div>
{% block content %}
{% endblock %}
<div class='bottom'>
<p>Created by <em>your name here</em>, 2022</p>
</div>
</body>
</html>
Let's add some styles too in the main.css
. Navigate to this link to see the CSS code snippet.
Let's now extend the base.html
in the index.html
file. We will also add a Hello World!
to the page as shown:
{% extends 'base.html' %}
{% block content %}
<div class='main'>
<h1>Hello World!</h1>
</div>
{% endblock %}
Then, we add a few more styles to the main div
in main.css
. You can check out this link for the stylings.
Now, in app.py
add the following code:
from flask import Flask, render_template
from flask_socketio import SocketIO, emit
from uuid import uuid4
app = Flask(__name__)
app.config['SECRET_KEY'] = uuid4().hex
socketio = SocketIO(app)
@app.route("/")
def hello_world():
return render_template('index.html')
From the above code:
- First, we import
Flask
andrender_template
. render_template
renders the.html
pages that we create.- We import the
flask_socketio
module to allow us to use Socket.io on the server, and listen to and emit events. - We import
uuid4
to get a unique secret key for the app. - We initialize the app with this secret key and enable Socket.io.
- Finally, we render
index.html
in the route"/"
.
Finally, add to wsgi.py
:
from app.app import app, socketio
if __name__ == "__main__":
socketio.run(app, debug=True)
Now, we run this file, which will run a local server with our app in debug mode.
You can run using the command python wsgi.py
in your terminal and navigate to http://localhost:5000
in your browser. You should see something like this:
The content of index.html
Now that we have created the base template, it's time to create the content of the page. Delete the <h1>Hello World!</h1>
from index.html
.
We need to add a div
with information about the game inside main
:
<div class='info'>
<h1>Welcome, Noble Adventurer!</h1>
<p>In <strong>Hunt the Wumpus</strong>, you are an adventurer entering a dungeon looking for a <em>treasure</em>.</p>
<p>Your goal is to find the treasure and get out of the dungeon <em>avoiding</em> the <strong>pits where you can fall in</strong> and the </strong>Wumpus</strong>: <em>a monster who kills <strong>anyone</strong> it sees.</em></p>
</div>
Add a form
where the player can select the dimensions of the dungeon, as shown below:
<div class='setup'>
<h1>Select the size of the dungeon</h1>
<form method='POST'>
<input type="number" id="height" class="size_input" name="height" min="5" max="10" value="5"/>
<large>X</large>
<input type="number" id="width" class="size_input" name="width" min="5" max="10" value="5"/>
<button class='play' type="submit">Enter the Dungeon</button>
</form>
</div>
We wil style the elements in main.css.
Refresh your page and now it should look like this:
But the page still doesn't do anything. Let's make it work!
Building the backend
Let's go back to app.py
. It's where the magic happens. We need to create sessions to store some user data using flask-session
as shown:
from flask import session
from flask_session import Session
The next step is to configure the session, making it permanent and setting the session type to the filesystem
. Add this to the beginning of your code:
app.config["SESSION_PERMANENT"] = True
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
In the above code snippet:
- We set the session to permanent. This means that the session cookies won't expire when the browser closes.
- We set the session type to the
filesystem
, which means that the cookies are going to be stored locally on the server-side.
The first thing we are going to store in the session is the user socket ID sid
. We store it when the user first connects to the page using @socketio.on('connect')
:
@socketio.on('connect')
def connect():
session['sid'] = request.sid
When the connect()
event happens, we store the user socket ID on the session variable sid
.
Now, let's get the data from the form
in index.html
. First, import request
from flask
:
from flask import request
We need to inform flask that we accept POST requests from /
route. Change the @app.route("/")
to @app.route("/", methods=['GET', 'POST'])
.
Let's also change the function name from hello_world()
to index()
. Inside index()
, we can check if there was a POST
request and retrieve the values from the form
.
@app.route("/", methods=['GET', 'POST'])
def index():
if request.method == 'POST':
session['width'] = int(request.form['width'])
session['height'] = int(request.form['height'])
return render_template('game.html')
return render_template('index.html')
When we receive a POST
request, we store the width
and height
of the dungeon in the session variables width
and height
. Then, we render the game.html
page.
Note that, we didn't create the
game.html
page yet.
Proceed to the templates/
directory and create it. Let's also extend the base.html
as shown:
{% extends 'base.html' %}
{% block content %}
<div class='main'>
<!--Nothing in here yet-->
</div>
{% endblock %}
If you refresh the page in your browser and click on the "Enter the Dungeon" button, the page game.html
will get rendered. There's nothing in game.html
yet, so you probably will see just a blank page with only the header and the footer.
Also, note that we stored the sizes of the dungeon but we didn't do anything with them. Let's use them to create the dungeon now. We are going to store the dungeon as a height
x width
matrix (a list of lists in Python).
Let's import random
so that we can generate the random positions of the Wumpus, the treasure, and the pits.
import random
Create a function called generate_dungeon(w, h)
. The parameter of the function: w
and h
are the width
and height
of the dungeon.
We store the dungeon in a session variable with random positions for the Wumpus and the treasure:
def generate_dungeon(w, h):
session['dungeon'] = []
wumpus_x = random.randrange(2, w)
wumpus_y = random.randrange(2, h)
gold_x = random.randrange(2, w)
gold_y = random.randrange(2, h)
while (gold_x == wumpus_x and gold_y == wumpus_y):
gold_x = random.randrange(2, w)
gold_y = random.randrange(2, h)
In the above code:
- We set the session variable
dungeon
to an empty list. - Then, we compute the random positions for the wumpus and treasure.
Note that we are getting random coordinates (x, y)
such that 2 <= x < width
and 2 <= y < height
.
This is because the player will start in the position (0, 0)
of the dungeon and if there's a wumpus or a treasure in the positions (0, 0)
or (0, 1)
or (1, 0)
or (1, 1)
, it would be very easy to win or lose the game.
So, let's avoid this by making the Wumpus and the treasure does not appear in these positions. It would also be very tragic if the Wumpus and the treasure appear in the same position. We are avoiding these scenarios with the while
loop.
Now, let's build our matrix:
for i in range(h):
line = []
# 0 - free
# 1 - pit
# 2 - wumpus
for j in range(w):
if (i == 0 or i == 1) and (j == 0 or j == 1):
# the beginning of the dungeon cannot have pits or wumpus
line.append(0)
elif i == wumpus_x and j == wumpus_y:
# wumpus location
line.append(2)
elif i == gold_x and j == gold_y:
# gold location
line.append(3)
elif ((i == wumpus_x - 1 or i == wumpus_x + 1) and (j == wumpus_y)) or ((j == wumpus_y - 1 or j == wumpus_y + 1) and (i == wumpus_x)):
# nothing around the wumpus
line.append(0)
else:
# random pits distributed
if random.random() <= 0.15:
line.append(1)
else:
line.append(0)
session['dungeon'].append(line)
In the above code, we position the Wumpus and the treasure, and we make the positions around the wumpus, free of pits.
The pits are positioned in the rest of the map with a probability of 15% of appearing in some random position.
Then, we store the player's initial position as the current position in the session. The generate_dungeon
function will call game.html
with the dungeon already created.
Add this code to the end of the function:
session['curr_pos'] = [0, 0]
return render_template('game.html', width=w, height=h)
We just store the player's current position in a session variable. As the player starts the game in position (0, 0)
, we store [0, 0]
in session variable curr_pos
.
Your code should appear as follows:
def generate_dungeon(w, h):
session['dungeon'] = []
wumpus_x = random.randrange(2, w)
wumpus_y = random.randrange(2, h)
gold_x = random.randrange(2, w)
gold_y = random.randrange(2, h)
while (gold_x == wumpus_x and gold_y == wumpus_y):
gold_x = random.randrange(2, w)
gold_y = random.randrange(2, h)
for i in range(h):
line = []
# 0 - free
# 1 - pit
# 2 - wumpus
for j in range(w):
if (i == 0 or i == 1) and (j == 0 or j == 1):
# the beginning of the dungeon cannot have pits or wumpus
line.append(0)
elif i == wumpus_x and j == wumpus_y:
# wumpus location
line.append(2)
elif i == gold_x and j == gold_y:
# gold location
line.append(3)
elif ((i == wumpus_x - 1 or i == wumpus_x + 1) and (j == wumpus_y)) or ((j == wumpus_y - 1 or j == wumpus_y + 1) and (i == wumpus_x)):
# nothing around the wumpus
line.append(0)
else:
# random pits distributed
if random.random() <= 0.15:
line.append(1)
else:
line.append(0)
session['dungeon'].append(line)
session['curr_pos'] = [0, 0]
return render_template('game.html', width=w, height=h)
In index()
, delete return render_template('game.html')
and add return generate_dungeon(session['width'], session['height'])
. This is how index()
should appear:
@app.route("/", methods=['GET', 'POST'])
def index():
if request.method == 'POST':
session['width'] = int(request.form['width'])
session['height'] = int(request.form['height'])
return generate_dungeon(session['width'], session['height'])
return render_template('index.html')
The function generate_dungeon()
renders the game.html
page, so there's no need to render it again in index()
.
Now when the player selects the dimensions of the dungeon and clicks on "Enter the Dungeon", we create the dungeon and render the game.html
by passing the width
and height
of the dungeon as arguments.
It's time to create the content of game.html
. Let's create two div
s: win
and lose
, with the messages that will appear when the player wins or loses the game. Inside .main
div add:
<div class="win">
<h4>Congratulations! You won!</h4>
<a href='/'><button class='play'>Play again</button></a>
</div>
<div class="lose">
<h4>Game Over!</h4>
<a href='/'><button class='play'>Play again</button></a>
</div>
Let's also create the dungeon map. Below the win
and lose
div
s, add:
{% for i in range(height) %}
<div class="line">
{% for j in range(width) %}
{% if i == 0 and j == 0 %}
<div class="room curr_room" x="{{j}}" y="{{i}}"></div>
{% else %}
<div class="room" x="{{j}}" y="{{i}}"></div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
We create a line for each i
in the range [0, height)
. Inside each line, we add a .room
. If the position is (0, 0)
, we make the .room
a .curr_room
.
Let's style all this components. Let's make the .curr_room
stand out from the other .room
s, since it differentiates the room that the player is currently in.
Let's also make .win
and .lose
hidden: they will appear only when the player wins or loses the game. Here is the CSS snippet for doing this.
Let's add the controls too. Include the following code below the for loop in game.html
:
<div class="controls">
<div class="left"><i data-feather="arrow-left"></i></div>
<div class="up_down">
<div class="up"><i data-feather="arrow-up"></i></div>
<div class="down"><i data-feather="arrow-down"></i></div>
</div>
<div class="right"><i data-feather="arrow-right"></i></div>
</div>
<script>
feather.replace()
</script>
They're simple controls with arrow keys. We shall add some stylings here.
Let's see how it is now:
Interacting with the game using JavaScript and Socket.io
Now, it's time to develop the interaction with the game. First, let's capture the actions of the player. Go to main.js
and add:
playing = true
$(document).ready(function(){
const socket = io();
//actions
$('.left').click(function(){
if (playing) socket.emit('next_pos', {'action':'left'})
})
$('.right').click(function(){
if (playing) socket.emit('next_pos', {'action':'right'})
})
$('.up').click(function(){
if (playing) socket.emit('next_pos', {'action':'up'})
})
$('.down').click(function(){
if (playing) socket.emit('next_pos', {'action':'down'})
})
})
In the above code, we emit the event next_pos
and specify the action
: left
, right
, up
or down
in the message.
Now, the Flask app in the server will receive the event next_pos
and the message. When this happens, the app needs to calculate the next position of the player, and send it back to the client. Go back to app.py
and add:
@socketio.on('next_pos')
def next_pos(message):
if message['action'] == 'left':
if 0 <= session['curr_pos'][0] - 1 < session['width']:
session['curr_pos'][0] -= 1
elif message['action'] == 'right':
if 0 <= session['curr_pos'][0] + 1 < session['width']:
session['curr_pos'][0] += 1
elif message['action'] == 'up':
if 0 <= session['curr_pos'][1] - 1 < session['height']:
session['curr_pos'][1] -= 1
elif message['action'] == 'down':
if 0 <= session['curr_pos'][1] + 1 < session['height']:
session['curr_pos'][1] += 1
elif message['action'] == 'arrow':
pass
pos = session['curr_pos']
socketio.emit('update_status',
{"x": pos[0],
"y": pos[1]},
room=session['sid'])
In the above code:
- When the server receives the
next_pos
event, it will update thecurr_pos
variable in the user session and send it back to the client usingsocketio.emit('update_status')
with the player's new coordinates in the message. - Note the line
room=session['sid']
. This is extremely important. This line says that the message will be sent only to the user's room. - If this line wasn't there, the event would be sent to every user, messing up the game of players who were playing simultaneously. You can test this by removing this line and playing the game with two or more devices simultaneously.
Now, navigate back to main.js
and let's handle the update_status
event. Inside the $(document).ready()
add:
socket.on('update_status', (data)=>{
curr = $(".curr_room")
curr.removeClass("curr_room")
room = $(".room[x='" + data['x'] + "'][y='" + data['y'] + "']")
room.addClass("curr_room")
})
We remove the .curr_room
style from the old player's room and add it to the new player's room. But, how will the player know where's the pits, the Wumpus, or the treasure?
Let's use the Feather icons to signalize this to the player.
First, we need to go back to app.py
and verify what is in the adjacency of the player. Inside next_pos()
, after the computation of the new position, let's add:
pos = session['curr_pos']
print(pos)
adj = []
pit = 0
wumpus = 0
treasure = 0
if session['dungeon'][pos[1]][pos[0]] == 1:
pit = 2
socketio.emit('lose', room=session['sid'])
elif session['dungeon'][pos[1]][pos[0]] == 2:
wumpus = 2
socketio.emit('lose', room=session['sid'])
elif session['dungeon'][pos[1]][pos[0]] == 3:
treasure = 2
socketio.emit('win', room=session['sid'])
else:
try:
adj.append(session['dungeon'][pos[1]-1][pos[0]])
except:
pass
try:
adj.append(session['dungeon'][pos[1]+1][pos[0]])
except:
pass
try:
adj.append(session['dungeon'][pos[1]][pos[0]-1])
except:
pass
try:
adj.append(session['dungeon'][pos[1]][pos[0]+1])
except:
pass
if 3 in adj:
treasure = 1
if 2 in adj:
wumpus = 1
if 1 in adj:
pit = 1
With this, we can track the player's adjacency:
- If
pit = 1
, then there's a pit around - If
wumpus = 1
, then the Wumpus is around - If
treasure = 1
, then the treasure is around - If
pit = 2
, then the player fell into a pit and lost the game - If
wumpus = 2
, then the player was eaten by the Wumpus and lost the game - If
treasure = 2
, then the player found the treasure and won the game
When the player loses or wins the game, we emit the lose
or win
event. We also need to pass what is in the player's adjacency to the client. Let's change the socketio.emit('update_status')
in the end of next_pos()
to:
socketio.emit('update_status',
{"x": pos[0],
"y": pos[1],
"pit": pit,
"wumpus": wumpus,
"treasure": treasure},
room=session['sid'])
The next_pos()
should be looking like this:
@socketio.on('next_pos')
def next_pos(message):
if message['action'] == 'left':
if 0 <= session['curr_pos'][0] - 1 < session['width']:
session['curr_pos'][0] -= 1
elif message['action'] == 'right':
if 0 <= session['curr_pos'][0] + 1 < session['width']:
session['curr_pos'][0] += 1
elif message['action'] == 'up':
if 0 <= session['curr_pos'][1] - 1 < session['height']:
session['curr_pos'][1] -= 1
elif message['action'] == 'down':
if 0 <= session['curr_pos'][1] + 1 < session['height']:
session['curr_pos'][1] += 1
elif message['action'] == 'arrow':
pass
pos = session['curr_pos']
adj = []
pit = 0
wumpus = 0
treasure = 0
if session['dungeon'][pos[1]][pos[0]] == 1:
pit = 2
socketio.emit('lose', room=session['sid'])
elif session['dungeon'][pos[1]][pos[0]] == 2:
wumpus = 2
socketio.emit('lose', room=session['sid'])
elif session['dungeon'][pos[1]][pos[0]] == 3:
treasure = 2
socketio.emit('win', room=session['sid'])
else:
try:
adj.append(session['dungeon'][pos[1]-1][pos[0]])
except:
pass
try:
adj.append(session['dungeon'][pos[1]+1][pos[0]])
except:
pass
try:
adj.append(session['dungeon'][pos[1]][pos[0]-1])
except:
pass
try:
adj.append(session['dungeon'][pos[1]][pos[0]+1])
except:
pass
if 3 in adj:
treasure = 1
if 2 in adj:
wumpus = 1
if 1 in adj:
pit = 1
socketio.emit('update_status',
{"x": pos[0],
"y": pos[1],
"pit": pit,
"wumpus": wumpus,
"treasure": treasure},
room=session['sid'])
Now, the client will know what is in the player's adjacent position and we can add the icons to signalize this. Navigate back to main.js
and let's handle this new data. Change the handle of update_status
in main.js
to:
socket.on('update_status', (data)=>{
curr = $(".curr_room")
curr.removeClass("curr_room")
room = $(".room[x='" + data['x'] + "'][y='" + data['y'] + "']")
room.empty()
if (data['pit'] == 2) {
room.append("<i data-feather='x-circle'></i>")
} else if (data['wumpus'] == 2) {
room.append("<i data-feather='frown'></i>")
} else if (data['treasure'] == 2) {
room.append("<i data-feather='award'></i>")
} else {
if (data['pit'] == 1) {
room.append("<i data-feather='wind'></i>")
}
if (data['wumpus'] == 1) {
room.append("<i data-feather='alert-triangle'></i>")
}
if (data['treasure'] == 1) {
room.append("<i data-feather='star'></i>")
}
}
feather.replace()
room.addClass("curr_room")
})
We add the following icons:
wind
icon when there's a pit nearby (data['pit'] == 1
)alert-triangle
icon when the Wumpus is nearby (data['wumpus'] == 1
)star
icon when the treasure is nearby (data['treasure'] == 1
)award
icon when the player finds the treasure (data['treasure'] == 2
)frown
icon when the player is killed by Wumpus (data['wumpus'] == 2
)x-circle
icon when the player falls in a pit (data['pit'] == 2
)
We need to handle the win
and lose
events too. Add this to $(document).ready()
:
// win or lose
socket.on('win', (data)=>{
$('.win').css("display", "flex")
$('.win').show("fast")
playing = false
})
socket.on('lose', (data)=>{
$('.lose').css("display", "flex")
$('.lose').show("fast")
playing = false
})
We show the .lose
or .win
div and set playing
to false
, making it impossible for the player to move.
Finally, let's just make it possible to move around the map using the keyboard by detecting the arrows keypress:
// detecting arrow keys
$(document).keydown(function(e) {
if (e.keyCode == 37) {
//left
if (playing) socket.emit('next_pos', {'action':'left'})
} else if (e.keyCode == 38) {
//up
if (playing) socket.emit('next_pos', {'action':'up'})
} else if (e.keyCode == 39) {
//right
if (playing) socket.emit('next_pos', {'action':'right'})
} else if (e.keyCode == 40) {
//down
if (playing) socket.emit('next_pos', {'action':'down'})
}
})
And that's it! The game is ready to play! Go on and test it. What do you think?
Conclusion
In this tutorial, you learned about the "Hunt the Wumpus" game and its influence on Artificial Intelligence.
We also developed a simple clone of the game using Python, Flask, HTML, CSS, JavaScript, and Socket.io.
You can see how it's possible to create games in a very easy way using these technologies. You can now explore the endless possibilities of applications that you can build with them!
Feel free to add additional functionalities to the game.
You can download this project from this GitHub repository.
Further reading
- Hunt the Wumpus - Wikipedia
- AI | The Wumpus World Description - GeeksforGeeks
- Artificial Intelligence: A Modern Approach
- Flask
- Socket.io
Peer Review Contributions by: Srishilesh P S