forked from davidflanagan/jstdg7
-
Notifications
You must be signed in to change notification settings - Fork 0
/
guessinggame.html
184 lines (166 loc) · 7.77 KB
/
guessinggame.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<html><head><title>I'm thinking of a number...</title>
<style>
body { height: 250px; display: flex; flex-direction: column;
align-items: center; justify-content: space-evenly; }
#heading { font: bold 36px sans-serif; margin: 0; }
#container { border: solid black 1px; height: 1em; width: 80%; }
#range { background-color: green; margin-left: 0%; height: 1em; width: 100%; }
#input { display: block; font-size: 24px; width: 60%; padding: 5px; }
#playagain { font-size: 24px; padding: 10px; border-radius: 5px; }
</style>
</head>
<body>
<h1 id="heading">I'm thinking of a number...</h1>
<!-- A visual representation of the numbers that have not been ruled out -->
<div id="container"><div id="range"></div></div>
<!-- Where the user enters their guess -->
<input id="input" type="text">
<!-- A button that reloads with no search string. Hidden until game ends. -->
<button id="playagain" hidden onclick="location.search='';">Play Again</button>
<script>
/**
* An instance of this GameState class represents the internal state of
* our number guessing game. The class defines static factory methods for
* initializing the game state from different sources, a method for
* updating the state based on a new guess, and a method for modifying the
* document based on the current state.
*/
class GameState {
// This is a factory function to create a new game
static newGame() {
let s = new GameState();
s.secret = s.randomInt(0, 100); // An integer: 0 < n < 100
s.low = 0; // Guesses must be greater than this
s.high = 100; // Guesses must be less than this
s.numGuesses = 0; // How many guesses have been made
s.guess = null; // What the last guess was
return s;
}
// When we save the state of the game with history.pushState(), it is just
// a plain JavaScript object that gets saved, not an instance of GameState.
// So this factory function re-creates a GameState object based on the
// plain object that we get from a popstate event.
static fromStateObject(stateObject) {
let s = new GameState();
for(let key of Object.keys(stateObject)) {
s[key] = stateObject[key];
}
return s;
}
// In order to enable bookmarking, we need to be able to encode the
// state of any game as a URL. This is easy to do with URLSearchParams.
toURL() {
let url = new URL(window.location);
url.searchParams.set("l", this.low);
url.searchParams.set("h", this.high);
url.searchParams.set("n", this.numGuesses);
url.searchParams.set("g", this.guess);
// Note that we can't encode the secret number in the url or it
// will give away the secret. If the user bookmarks the page with
// these parameters and then returns to it, we will simply pick a
// new random number between low and high.
return url.href;
}
// This is a factory function that creates a new GameState object and
// initializes it from the specified URL. If the URL does not contain the
// expected parameters or if they are malformed it just returns null.
static fromURL(url) {
let s = new GameState();
let params = new URL(url).searchParams;
s.low = parseInt(params.get("l"));
s.high = parseInt(params.get("h"));
s.numGuesses = parseInt(params.get("n"));
s.guess = parseInt(params.get("g"));
// If the URL is missing any of the parameters we need or if
// they did not parse as integers, then return null;
if (isNaN(s.low) || isNaN(s.high) ||
isNaN(s.numGuesses) || isNaN(s.guess)) {
return null;
}
// Pick a new secret number in the right range each time we
// restore a game from a URL.
s.secret = s.randomInt(s.low, s.high);
return s;
}
// Return an integer n, min < n < max
randomInt(min, max) {
return min + Math.ceil(Math.random() * (max - min - 1));
}
// Modify the document to display the current state of the game.
render() {
let heading = document.querySelector("#heading"); // The <h1> at the top
let range = document.querySelector("#range"); // Display guess range
let input = document.querySelector("#input"); // Guess input field
let playagain = document.querySelector("#playagain");
// Update the document heading and title
heading.textContent = document.title =
`I'm thinking of a number between ${this.low} and ${this.high}.`;
// Update the visual range of numbers
range.style.marginLeft = `${this.low}%`;
range.style.width = `${(this.high-this.low)}%`;
// Make sure the input field is empty and focused.
input.value = "";
input.focus();
// Display feedback based on the user's last guess. The input
// placeholder will show because we made the input field empty.
if (this.guess === null) {
input.placeholder = "Type your guess and hit Enter";
} else if (this.guess < this.secret) {
input.placeholder = `${this.guess} is too low. Guess again`;
} else if (this.guess > this.secret) {
input.placeholder = `${this.guess} is too high. Guess again`;
} else {
input.placeholder = document.title = `${this.guess} is correct!`;
heading.textContent = `You win in ${this.numGuesses} guesses!`;
playagain.hidden = false;
}
}
// Update the state of the game based on what the user guessed.
// Returns true if the state was updated, and false otherwise.
updateForGuess(guess) {
// If it is a number and is in the right range
if ((guess > this.low) && (guess < this.high)) {
// Update state object based on this guess
if (guess < this.secret) this.low = guess;
else if (guess > this.secret) this.high = guess;
this.guess = guess;
this.numGuesses++;
return true;
}
else { // An invalid guess: notify user but don't update state
alert(`Please enter a number greater than ${
this.low} and less than ${this.high}`);
return false;
}
}
}
// With the GameState class defined, making the game work is just a matter
// of initializing, updating, saving and rendering the state object at
// the appropriate times.
// When we are first loaded, we try get the state of the game from the URL
// and if that fails we instead begin a new game. So if the user bookmarks a
// game that game can be restored from the URL. But if we load a page with
// no query parameters we'll just get a new game.
let gamestate = GameState.fromURL(window.location) || GameState.newGame();
// Save this initial state of the game into the browser history, but use
// replaceState instead of pushState() for this initial page
history.replaceState(gamestate, "", gamestate.toURL());
// Display this initial state
gamestate.render();
// When the user guesses, update the state of the game based on their guess
// then save the new state to browser history and render the new state
document.querySelector("#input").onchange = (event) => {
if (gamestate.updateForGuess(parseInt(event.target.value))) {
history.pushState(gamestate, "", gamestate.toURL());
}
gamestate.render();
};
// If the user goes back or forward in history, we'll get a popstate event
// on the window object with a copy of the state object we saved with
// pushState. When that happens, render the new state.
window.onpopstate = (event) => {
gamestate = GameState.fromStateObject(event.state); // Restore the state
gamestate.render(); // and display it
};
</script>
</body></html>